diff --git a/pydantic_ai_slim/pydantic_ai/models/__init__.py b/pydantic_ai_slim/pydantic_ai/models/__init__.py index aef137cc29..aef39920fa 100644 --- a/pydantic_ai_slim/pydantic_ai/models/__init__.py +++ b/pydantic_ai_slim/pydantic_ai/models/__init__.py @@ -545,6 +545,90 @@ def prompted_output_instructions(self) -> str | None: __repr__ = _utils.dataclasses_no_defaults_repr +@dataclass +class _ResolvedToolChoice: + """Provider-agnostic resolved tool choice. + + This is the result of validating and resolving the user's `tool_choice` setting. + Providers should map this to their API-specific format. + """ + + mode: Literal['none', 'auto', 'required', 'specific'] + """The resolved tool choice mode.""" + + tool_names: list[str] = field(default_factory=list) + """For 'specific' mode, the list of tool names to force. Empty for other modes.""" + + def filter_tools( + self, + function_tools: list[ToolDefinition], + output_tools: list[ToolDefinition], + ) -> list[ToolDefinition]: + """Filter tools based on the resolved mode. + + - 'none': only output_tools + - 'required': only function_tools + - 'specific': specified function_tools + output_tools + - 'auto': all tools + """ + if self.mode == 'none': + return list(output_tools) + elif self.mode == 'required': + return list(function_tools) + elif self.mode == 'specific': + allowed = set(self.tool_names) + return [t for t in function_tools if t.name in allowed] + list(output_tools) + else: # 'auto' + return [*function_tools, *output_tools] + + +def _resolve_tool_choice( # pyright: ignore[reportUnusedFunction] + model_settings: ModelSettings | None, + model_request_parameters: ModelRequestParameters, +) -> _ResolvedToolChoice | None: + """Resolve and validate tool_choice from model settings. + + This centralizes the common logic for handling tool_choice across all providers: + - Validates tool names in list[str] against available function_tools + - Returns a provider-agnostic _ResolvedToolChoice for the provider to map to their API format + + Args: + model_settings: The model settings containing tool_choice. + model_request_parameters: The request parameters containing tool definitions. + + Returns: + _ResolvedToolChoice if an explicit tool_choice was provided and validated, + None if tool_choice was not set (provider should use default behavior based on allow_text_output). + + Raises: + UserError: If tool names in list[str] are invalid. + """ + user_tool_choice = (model_settings or {}).get('tool_choice') + + if user_tool_choice is None: + return None + + if user_tool_choice == 'none': + return _ResolvedToolChoice(mode='none') + + if user_tool_choice in ('auto', 'required'): + return _ResolvedToolChoice(mode=user_tool_choice) + + if isinstance(user_tool_choice, list): + if not user_tool_choice: + return _ResolvedToolChoice(mode='none') + function_tool_names = {t.name for t in model_request_parameters.function_tools} + invalid_names = set(user_tool_choice) - function_tool_names + if invalid_names: + raise UserError( + f'Invalid tool names in `tool_choice`: {invalid_names}. ' + f'Available function tools: {function_tool_names or "none"}' + ) + return _ResolvedToolChoice(mode='specific', tool_names=list(user_tool_choice)) + + return None # pragma: no cover + + class Model(ABC): """Abstract class for a model.""" diff --git a/pydantic_ai_slim/pydantic_ai/models/anthropic.py b/pydantic_ai_slim/pydantic_ai/models/anthropic.py index 2c7ff578d5..cde31928f6 100644 --- a/pydantic_ai_slim/pydantic_ai/models/anthropic.py +++ b/pydantic_ai_slim/pydantic_ai/models/anthropic.py @@ -1,6 +1,7 @@ from __future__ import annotations as _annotations import io +import warnings from collections.abc import AsyncGenerator, AsyncIterable, AsyncIterator from contextlib import asynccontextmanager from dataclasses import dataclass, field, replace @@ -42,7 +43,15 @@ from ..providers.anthropic import AsyncAnthropicClient from ..settings import ModelSettings, merge_model_settings from ..tools import ToolDefinition -from . import Model, ModelRequestParameters, StreamedResponse, check_allow_model_requests, download_item, get_user_agent +from . import ( + Model, + ModelRequestParameters, + StreamedResponse, + _resolve_tool_choice, # pyright: ignore[reportPrivateUsage] + check_allow_model_requests, + download_item, + get_user_agent, +) _FINISH_REASON_MAP: dict[BetaStopReason, FinishReason] = { 'end_turn': 'stop', @@ -386,11 +395,9 @@ async def _messages_create( This is the last step before sending the request to the API. Most preprocessing has happened in `prepare_request()`. """ - tools = self._get_tools(model_request_parameters, model_settings) + tools, tool_choice = self._infer_tool_choice(model_settings, model_request_parameters) tools, mcp_servers, builtin_tool_betas = self._add_builtin_tools(tools, model_request_parameters) - tool_choice = self._infer_tool_choice(tools, model_settings, model_request_parameters) - system_prompt, anthropic_messages = await self._map_message(messages, model_request_parameters, model_settings) self._limit_cache_points(system_prompt, anthropic_messages, tools) output_format = self._native_output_format(model_request_parameters) @@ -474,11 +481,9 @@ async def _messages_count_tokens( raise UserError('AsyncAnthropicBedrock client does not support `count_tokens` api.') # standalone function to make it easier to override - tools = self._get_tools(model_request_parameters, model_settings) + tools, tool_choice = self._infer_tool_choice(model_settings, model_request_parameters) tools, mcp_servers, builtin_tool_betas = self._add_builtin_tools(tools, model_request_parameters) - tool_choice = self._infer_tool_choice(tools, model_settings, model_request_parameters) - system_prompt, anthropic_messages = await self._map_message(messages, model_request_parameters, model_settings) self._limit_cache_points(system_prompt, anthropic_messages, tools) output_format = self._native_output_format(model_request_parameters) @@ -584,22 +589,6 @@ async def _process_streamed_response( _provider_url=self._provider.base_url, ) - def _get_tools( - self, model_request_parameters: ModelRequestParameters, model_settings: AnthropicModelSettings - ) -> list[BetaToolUnionParam]: - tools: list[BetaToolUnionParam] = [ - self._map_tool_definition(r) for r in model_request_parameters.tool_defs.values() - ] - - # Add cache_control to the last tool if enabled - if tools and (cache_tool_defs := model_settings.get('anthropic_cache_tool_definitions')): - # If True, use '5m'; otherwise use the specified ttl value - ttl: Literal['5m', '1h'] = '5m' if cache_tool_defs is True else cache_tool_defs - last_tool = tools[-1] - last_tool['cache_control'] = self._build_cache_control(ttl) - - return tools - def _add_builtin_tools( self, tools: list[BetaToolUnionParam], model_request_parameters: ModelRequestParameters ) -> tuple[list[BetaToolUnionParam], list[BetaRequestMCPServerURLDefinitionParam], set[str]]: @@ -663,26 +652,91 @@ def _add_builtin_tools( ) return tools, mcp_servers, beta_features - def _infer_tool_choice( + def _infer_tool_choice( # noqa: C901 self, - tools: list[BetaToolUnionParam], model_settings: AnthropicModelSettings, model_request_parameters: ModelRequestParameters, - ) -> BetaToolChoiceParam | None: - if not tools: - return None + ) -> tuple[list[BetaToolUnionParam], BetaToolChoiceParam | None]: + """Determine which tools to send and the API tool_choice value. + + Returns: + A tuple of (filtered_tools, tool_choice). + """ + thinking_enabled = model_settings.get('anthropic_thinking') is not None + function_tools = model_request_parameters.function_tools + output_tools = model_request_parameters.output_tools + + resolved = _resolve_tool_choice(model_settings, model_request_parameters) + + if resolved is None: + tool_defs_to_send = [*function_tools, *output_tools] else: - tool_choice: BetaToolChoiceParam + tool_defs_to_send = resolved.filter_tools(function_tools, output_tools) + + # Map ToolDefinitions to Anthropic format + tools: list[BetaToolUnionParam] = [self._map_tool_definition(t) for t in tool_defs_to_send] + + # Add cache_control to the last tool if enabled + if tools and (cache_tool_defs := model_settings.get('anthropic_cache_tool_definitions')): + ttl: Literal['5m', '1h'] = '5m' if cache_tool_defs is True else cache_tool_defs + last_tool = tools[-1] + last_tool['cache_control'] = self._build_cache_control(ttl) + + if not tools: + return tools, None + tool_choice: BetaToolChoiceParam + + if resolved is None: if not model_request_parameters.allow_text_output: tool_choice = {'type': 'any'} else: tool_choice = {'type': 'auto'} - if 'parallel_tool_calls' in model_settings: - tool_choice['disable_parallel_tool_use'] = not model_settings['parallel_tool_calls'] + elif resolved.mode == 'auto': + if not model_request_parameters.allow_text_output: + tool_choice = {'type': 'any'} + else: + tool_choice = {'type': 'auto'} + + elif resolved.mode == 'required': + if thinking_enabled: + raise UserError( + "tool_choice='required' is not supported with Anthropic thinking mode. " + 'Use `output_type=NativeOutput(...)` or `PromptedOutput(...)` instead.' + ) + tool_choice = {'type': 'any'} + + elif resolved.mode == 'none': + if len(output_tools) == 1: + tool_choice = {'type': 'tool', 'name': output_tools[0].name} + else: + warnings.warn( + "Anthropic only supports forcing a single tool. Falling back to 'auto' for multiple output tools." + ) + tool_choice = {'type': 'auto'} + + elif resolved.mode == 'specific': + if thinking_enabled: + raise UserError( + 'Forcing specific tools is not supported with Anthropic thinking mode. ' + 'Use `output_type=NativeOutput(...)` or `PromptedOutput(...)` instead.' + ) + if len(resolved.tool_names) == 1: + tool_choice = {'type': 'tool', 'name': resolved.tool_names[0]} + else: + warnings.warn( + "Anthropic only supports forcing a single tool. Falling back to 'any' for multiple specific tools." + ) + tool_choice = {'type': 'any'} + + else: + assert_never(resolved.mode) + + if 'parallel_tool_calls' in model_settings: + tool_choice['disable_parallel_tool_use'] = not model_settings['parallel_tool_calls'] - return tool_choice + return tools, tool_choice async def _map_message( # noqa: C901 self, @@ -887,9 +941,10 @@ async def _map_message( # noqa: C901 system_prompt_parts.insert(0, instructions) system_prompt = '\n\n'.join(system_prompt_parts) + ttl: Literal['5m', '1h'] # Add cache_control to the last message content if anthropic_cache_messages is enabled if anthropic_messages and (cache_messages := model_settings.get('anthropic_cache_messages')): - ttl: Literal['5m', '1h'] = '5m' if cache_messages is True else cache_messages + ttl = '5m' if cache_messages is True else cache_messages m = anthropic_messages[-1] content = m['content'] if isinstance(content, str): @@ -909,7 +964,7 @@ async def _map_message( # noqa: C901 # If anthropic_cache_instructions is enabled, return system prompt as a list with cache_control if system_prompt and (cache_instructions := model_settings.get('anthropic_cache_instructions')): # If True, use '5m'; otherwise use the specified ttl value - ttl: Literal['5m', '1h'] = '5m' if cache_instructions is True else cache_instructions + ttl = '5m' if cache_instructions is True else cache_instructions system_prompt_blocks = [ BetaTextBlockParam( type='text', diff --git a/pydantic_ai_slim/pydantic_ai/models/bedrock.py b/pydantic_ai_slim/pydantic_ai/models/bedrock.py index 1424d2cb58..50a83333a8 100644 --- a/pydantic_ai_slim/pydantic_ai/models/bedrock.py +++ b/pydantic_ai_slim/pydantic_ai/models/bedrock.py @@ -2,6 +2,7 @@ import functools import typing +import warnings from collections.abc import AsyncIterator, Iterable, Iterator, Mapping from contextlib import asynccontextmanager from dataclasses import dataclass, field @@ -41,7 +42,13 @@ ) from pydantic_ai._run_context import RunContext from pydantic_ai.exceptions import ModelAPIError, ModelHTTPError, UserError -from pydantic_ai.models import Model, ModelRequestParameters, StreamedResponse, download_item +from pydantic_ai.models import ( + Model, + ModelRequestParameters, + StreamedResponse, + _resolve_tool_choice, # pyright: ignore[reportPrivateUsage] + download_item, +) from pydantic_ai.providers import Provider, infer_provider from pydantic_ai.providers.bedrock import BEDROCK_GEO_PREFIXES, BedrockModelProfile from pydantic_ai.settings import ModelSettings @@ -254,9 +261,6 @@ def system(self) -> str: """The model provider.""" return self._provider.name - def _get_tools(self, model_request_parameters: ModelRequestParameters) -> list[ToolTypeDef]: - return [self._map_tool_definition(r) for r in model_request_parameters.tool_defs.values()] - @staticmethod def _map_tool_definition(f: ToolDefinition) -> ToolTypeDef: tool_spec: ToolSpecificationTypeDef = {'name': f.name, 'inputSchema': {'json': f.parameters_json_schema}} @@ -422,7 +426,7 @@ async def _messages_create( 'inferenceConfig': inference_config, } - tool_config = self._map_tool_config(model_request_parameters) + tool_config = self._map_tool_config(model_request_parameters, model_settings) if tool_config: params['toolConfig'] = tool_config @@ -478,17 +482,58 @@ def _map_inference_config( return inference_config - def _map_tool_config(self, model_request_parameters: ModelRequestParameters) -> ToolConfigurationTypeDef | None: - tools = self._get_tools(model_request_parameters) - if not tools: + def _map_tool_config( + self, + model_request_parameters: ModelRequestParameters, + model_settings: BedrockModelSettings | None, + ) -> ToolConfigurationTypeDef | None: + resolved = _resolve_tool_choice(model_settings, model_request_parameters) + function_tools = model_request_parameters.function_tools + output_tools = model_request_parameters.output_tools + + if resolved is None: + tool_defs_to_send = [*function_tools, *output_tools] + else: + tool_defs_to_send = resolved.filter_tools(function_tools, output_tools) + + if not tool_defs_to_send: return None + tools = [self._map_tool_definition(t) for t in tool_defs_to_send] tool_choice: ToolChoiceTypeDef - if not model_request_parameters.allow_text_output: + + if resolved is None: + # Default behavior: infer from allow_text_output + if not model_request_parameters.allow_text_output: + tool_choice = {'any': {}} + else: + tool_choice = {'auto': {}} + + elif resolved.mode == 'auto': + if not model_request_parameters.allow_text_output: + tool_choice = {'any': {}} + else: + tool_choice = {'auto': {}} + + elif resolved.mode == 'required': tool_choice = {'any': {}} - else: + + elif resolved.mode == 'none': + # We've already filtered to only output tools, use 'auto' to let model choose tool_choice = {'auto': {}} + elif resolved.mode == 'specific': + if not resolved.tool_names: # pragma: no cover + raise RuntimeError('Internal error: resolved.tool_names is empty for specific tool choice.') + if len(resolved.tool_names) == 1: + tool_choice = {'tool': {'name': resolved.tool_names[0]}} + else: + warnings.warn("Bedrock only supports forcing a single tool. Falling back to 'any'.") + tool_choice = {'any': {}} + + else: + assert_never(resolved.mode) + tool_config: ToolConfigurationTypeDef = {'tools': tools} if tool_choice and BedrockModelProfile.from_profile(self.profile).bedrock_supports_tool_choice: tool_config['toolChoice'] = tool_choice diff --git a/pydantic_ai_slim/pydantic_ai/models/google.py b/pydantic_ai_slim/pydantic_ai/models/google.py index bf3ba0206d..0a7458037b 100644 --- a/pydantic_ai_slim/pydantic_ai/models/google.py +++ b/pydantic_ai_slim/pydantic_ai/models/google.py @@ -46,6 +46,7 @@ Model, ModelRequestParameters, StreamedResponse, + _resolve_tool_choice, # pyright: ignore[reportPrivateUsage] check_allow_model_requests, download_item, get_user_agent, @@ -336,16 +337,16 @@ async def request_stream( response = await self._generate_content(messages, True, model_settings, model_request_parameters) yield await self._process_streamed_response(response, model_request_parameters) # type: ignore - def _get_tools( + def _get_builtin_tools( self, model_request_parameters: ModelRequestParameters - ) -> tuple[list[ToolDict] | None, ImageConfigDict | None]: - tools: list[ToolDict] = [ - ToolDict(function_declarations=[_function_declaration_from_tool(t)]) - for t in model_request_parameters.tool_defs.values() - ] + ) -> tuple[list[ToolDict], ImageConfigDict | None]: + """Get Google-specific builtin tools (web search, code execution, etc.). + Returns: + A tuple of (builtin_tools, image_config). + """ + tools: list[ToolDict] = [] image_config: ImageConfigDict | None = None - if model_request_parameters.builtin_tools: if model_request_parameters.function_tools: raise UserError('Google does not support function tools and built-in tools at the same time.') @@ -368,20 +369,82 @@ def _get_tools( raise UserError( f'`{tool.__class__.__name__}` is not supported by `GoogleModel`. If it should be, please file an issue.' ) - return tools or None, image_config + return tools, image_config def _get_tool_config( - self, model_request_parameters: ModelRequestParameters, tools: list[ToolDict] | None - ) -> ToolConfigDict | None: - if not model_request_parameters.allow_text_output and tools: - names: list[str] = [] - for tool in tools: - for function_declaration in tool.get('function_declarations') or []: - if name := function_declaration.get('name'): # pragma: no branch - names.append(name) - return _tool_config(names) + self, + model_request_parameters: ModelRequestParameters, + model_settings: GoogleModelSettings, + ) -> tuple[list[ToolDict] | None, ToolConfigDict | None, ImageConfigDict | None]: + """Determine which tools to send and the API tool config. + + Returns: + A tuple of (filtered_tools, tool_config, image_config). + """ + function_tools = model_request_parameters.function_tools + output_tools = model_request_parameters.output_tools + builtin_tools, image_config = self._get_builtin_tools(model_request_parameters) + + resolved = _resolve_tool_choice(model_settings, model_request_parameters) + + if resolved is None: + tool_defs_to_send = [*function_tools, *output_tools] else: - return None + tool_defs_to_send = resolved.filter_tools(function_tools, output_tools) + tools: list[ToolDict] = [ + ToolDict(function_declarations=[_function_declaration_from_tool(t)]) for t in tool_defs_to_send + ] + + tools.extend(builtin_tools) + + if not tools: + return None, None, image_config + + tool_config: ToolConfigDict | None + + if resolved is None: + if not model_request_parameters.allow_text_output: + tool_config = ToolConfigDict( + function_calling_config=FunctionCallingConfigDict(mode=FunctionCallingConfigMode.ANY) + ) + else: + tool_config = None + + elif resolved.mode == 'auto': + if not model_request_parameters.allow_text_output: + tool_config = ToolConfigDict( + function_calling_config=FunctionCallingConfigDict(mode=FunctionCallingConfigMode.ANY) + ) + else: + tool_config = ToolConfigDict( + function_calling_config=FunctionCallingConfigDict(mode=FunctionCallingConfigMode.AUTO) + ) + + elif resolved.mode == 'required': + tool_config = ToolConfigDict( + function_calling_config=FunctionCallingConfigDict(mode=FunctionCallingConfigMode.ANY) + ) + + elif resolved.mode == 'none': + tool_config = ToolConfigDict( + function_calling_config=FunctionCallingConfigDict( + mode=FunctionCallingConfigMode.ANY, + allowed_function_names=[t.name for t in output_tools], + ) + ) + + elif resolved.mode == 'specific': + tool_config = ToolConfigDict( + function_calling_config=FunctionCallingConfigDict( + mode=FunctionCallingConfigMode.ANY, + allowed_function_names=resolved.tool_names, + ) + ) + + else: + assert_never(resolved.mode) + + return tools, tool_config, image_config @overload async def _generate_content( @@ -427,7 +490,7 @@ async def _build_content_and_config( model_settings: GoogleModelSettings, model_request_parameters: ModelRequestParameters, ) -> tuple[list[ContentUnionDict], GenerateContentConfigDict]: - tools, image_config = self._get_tools(model_request_parameters) + tools, tool_config, image_config = self._get_tool_config(model_request_parameters, model_settings) if model_request_parameters.function_tools and not self.profile.supports_tools: raise UserError('Tools are not supported by this model.') @@ -446,8 +509,6 @@ async def _build_content_and_config( if not self.profile.supports_json_object_output: raise UserError('JSON output is not supported by this model.') response_mime_type = 'application/json' - - tool_config = self._get_tool_config(model_request_parameters, tools) system_instruction, contents = await self._map_messages(messages, model_request_parameters) modalities = [Modality.TEXT.value] @@ -950,12 +1011,6 @@ def _function_declaration_from_tool(tool: ToolDefinition) -> FunctionDeclaration return f -def _tool_config(function_names: list[str]) -> ToolConfigDict: - mode = FunctionCallingConfigMode.ANY - function_calling_config = FunctionCallingConfigDict(mode=mode, allowed_function_names=function_names) - return ToolConfigDict(function_calling_config=function_calling_config) - - def _metadata_as_usage(response: GenerateContentResponse, provider: str, provider_url: str) -> usage.RequestUsage: metadata = response.usage_metadata if metadata is None: diff --git a/pydantic_ai_slim/pydantic_ai/models/groq.py b/pydantic_ai_slim/pydantic_ai/models/groq.py index 780ee0b305..66428bafae 100644 --- a/pydantic_ai_slim/pydantic_ai/models/groq.py +++ b/pydantic_ai_slim/pydantic_ai/models/groq.py @@ -1,5 +1,6 @@ from __future__ import annotations as _annotations +import warnings from collections.abc import AsyncIterable, AsyncIterator, Iterable from contextlib import asynccontextmanager from dataclasses import dataclass, field @@ -47,6 +48,7 @@ Model, ModelRequestParameters, StreamedResponse, + _resolve_tool_choice, # pyright: ignore[reportPrivateUsage] check_allow_model_requests, get_user_agent, ) @@ -56,6 +58,8 @@ from groq.types import chat from groq.types.chat.chat_completion_content_part_image_param import ImageURL from groq.types.chat.chat_completion_message import ExecutedTool + from groq.types.chat.chat_completion_named_tool_choice_param import ChatCompletionNamedToolChoiceParam + from groq.types.chat.chat_completion_tool_choice_option_param import ChatCompletionToolChoiceOptionParam except ImportError as _import_error: raise ImportError( 'Please install `groq` to use the Groq model, ' @@ -263,14 +267,8 @@ async def _completions_create( model_settings: GroqModelSettings, model_request_parameters: ModelRequestParameters, ) -> chat.ChatCompletion | AsyncStream[chat.ChatCompletionChunk]: - tools = self._get_tools(model_request_parameters) + tools, tool_choice = self._get_tool_choice(model_settings, model_request_parameters) tools += self._get_builtin_tools(model_request_parameters) - if not tools: - tool_choice: Literal['none', 'required', 'auto'] | None = None - elif not model_request_parameters.allow_text_output: - tool_choice = 'required' - else: - tool_choice = 'auto' groq_messages = self._map_messages(messages, model_request_parameters) @@ -373,8 +371,67 @@ async def _process_streamed_response( _provider_name=self._provider.name, ) - def _get_tools(self, model_request_parameters: ModelRequestParameters) -> list[chat.ChatCompletionToolParam]: - return [self._map_tool_definition(r) for r in model_request_parameters.tool_defs.values()] + def _get_tool_choice( + self, + model_settings: GroqModelSettings, + model_request_parameters: ModelRequestParameters, + ) -> tuple[list[chat.ChatCompletionToolParam], ChatCompletionToolChoiceOptionParam | None]: + """Determine which tools to send and the API tool_choice value. + + Returns: + A tuple of (filtered_tools, tool_choice). + """ + function_tools = model_request_parameters.function_tools + output_tools = model_request_parameters.output_tools + + resolved = _resolve_tool_choice(model_settings, model_request_parameters) + + if resolved is None: + tool_defs_to_send = [*function_tools, *output_tools] + else: + tool_defs_to_send = resolved.filter_tools(function_tools, output_tools) + tools: list[chat.ChatCompletionToolParam] = [self._map_tool_definition(t) for t in tool_defs_to_send] + + if not tools: + return tools, None + + tool_choice: ChatCompletionToolChoiceOptionParam + + if resolved is None: + if not model_request_parameters.allow_text_output: + tool_choice = 'required' + else: + tool_choice = 'auto' + + elif resolved.mode == 'auto': + if not model_request_parameters.allow_text_output: + tool_choice = 'required' + else: + tool_choice = 'auto' + + elif resolved.mode == 'required': + tool_choice = 'required' + + elif resolved.mode == 'none': + tool_choice = ChatCompletionNamedToolChoiceParam( + type='function', + function={'name': output_tools[0].name}, + ) + + elif resolved.mode == 'specific': + if len(resolved.tool_names) == 1: + tool_choice = ChatCompletionNamedToolChoiceParam( + type='function', + function={'name': resolved.tool_names[0]}, + ) + else: + warnings.warn("Groq only supports forcing a single tool. Falling back to 'required'.") + tool_choice = 'required' + + else: + assert_never(resolved.mode) + + return tools, tool_choice def _get_builtin_tools( self, model_request_parameters: ModelRequestParameters diff --git a/pydantic_ai_slim/pydantic_ai/models/huggingface.py b/pydantic_ai_slim/pydantic_ai/models/huggingface.py index f439b3ccb6..b29641bafe 100644 --- a/pydantic_ai_slim/pydantic_ai/models/huggingface.py +++ b/pydantic_ai_slim/pydantic_ai/models/huggingface.py @@ -45,6 +45,7 @@ Model, ModelRequestParameters, StreamedResponse, + _resolve_tool_choice, # pyright: ignore[reportPrivateUsage] check_allow_model_requests, ) @@ -52,10 +53,12 @@ import aiohttp from huggingface_hub import ( AsyncInferenceClient, + ChatCompletionInputFunctionName, ChatCompletionInputMessage, ChatCompletionInputMessageChunk, ChatCompletionInputTool, ChatCompletionInputToolCall, + ChatCompletionInputToolChoiceClass, ChatCompletionInputURL, ChatCompletionOutput, ChatCompletionOutputMessage, @@ -220,14 +223,7 @@ async def _completions_create( model_settings: HuggingFaceModelSettings, model_request_parameters: ModelRequestParameters, ) -> ChatCompletionOutput | AsyncIterable[ChatCompletionStreamOutput]: - tools = self._get_tools(model_request_parameters) - - if not tools: - tool_choice: Literal['none', 'required', 'auto'] | None = None - elif not model_request_parameters.allow_text_output: - tool_choice = 'required' - else: - tool_choice = 'auto' + tools, tool_choice = self._get_tool_choice(model_settings, model_request_parameters) if model_request_parameters.builtin_tools: raise UserError('HuggingFace does not support built-in tools') @@ -319,8 +315,63 @@ async def _process_streamed_response( _provider_name=self._provider.name, ) - def _get_tools(self, model_request_parameters: ModelRequestParameters) -> list[ChatCompletionInputTool]: - return [self._map_tool_definition(r) for r in model_request_parameters.tool_defs.values()] + def _get_tool_choice( + self, + model_settings: HuggingFaceModelSettings, + model_request_parameters: ModelRequestParameters, + ) -> tuple[ + list[ChatCompletionInputTool], + Literal['none', 'required', 'auto'] | ChatCompletionInputToolChoiceClass | None, + ]: + """Get tools and tool choice for the model. + + Returns a tuple of (tools, tool_choice). + """ + resolved = _resolve_tool_choice(model_settings, model_request_parameters) + function_tools = model_request_parameters.function_tools + output_tools = model_request_parameters.output_tools + + if resolved is None: + tool_defs_to_send = [*function_tools, *output_tools] + else: + tool_defs_to_send = resolved.filter_tools(function_tools, output_tools) + + if not tool_defs_to_send: + return [], None + + tools = [self._map_tool_definition(r) for r in tool_defs_to_send] + tool_choice: Literal['none', 'required', 'auto'] | ChatCompletionInputToolChoiceClass | None + + # Determine tool_choice value + if resolved is None: + # Default behavior: infer from allow_text_output + if not model_request_parameters.allow_text_output: + tool_choice = 'required' + else: + tool_choice = 'auto' + elif resolved.mode == 'auto': + if not model_request_parameters.allow_text_output: + tool_choice = 'required' + else: + tool_choice = 'auto' + elif resolved.mode == 'required': + tool_choice = 'required' + elif resolved.mode == 'none': + tool_choice = ChatCompletionInputToolChoiceClass( + function=ChatCompletionInputFunctionName(name=output_tools[0].name) # pyright: ignore[reportCallIssue] + ) + elif resolved.mode == 'specific': + if len(resolved.tool_names) == 1: + tool_choice = ChatCompletionInputToolChoiceClass( + function=ChatCompletionInputFunctionName(name=resolved.tool_names[0]) # pyright: ignore[reportCallIssue] + ) + else: + # HuggingFace only supports forcing a single tool + tool_choice = 'required' + else: + assert_never(resolved.mode) + + return tools, tool_choice async def _map_messages( self, messages: list[ModelMessage], model_request_parameters: ModelRequestParameters diff --git a/pydantic_ai_slim/pydantic_ai/models/mistral.py b/pydantic_ai_slim/pydantic_ai/models/mistral.py index 2a3752c370..88d3fdb685 100644 --- a/pydantic_ai_slim/pydantic_ai/models/mistral.py +++ b/pydantic_ai_slim/pydantic_ai/models/mistral.py @@ -45,6 +45,7 @@ Model, ModelRequestParameters, StreamedResponse, + _resolve_tool_choice, # pyright: ignore[reportPrivateUsage] check_allow_model_requests, get_user_agent, ) @@ -227,13 +228,15 @@ async def _completions_create( if model_request_parameters.builtin_tools: raise UserError('Mistral does not support built-in tools') + tools, tool_choice = self._get_tool_choice(model_request_parameters, model_settings) + try: response = await self.client.chat.complete_async( model=str(self._model_name), messages=self._map_messages(messages, model_request_parameters), n=1, - tools=self._map_function_and_output_tools_definition(model_request_parameters) or UNSET, - tool_choice=self._get_tool_choice(model_request_parameters), + tools=tools or UNSET, + tool_choice=tool_choice, stream=False, max_tokens=model_settings.get('max_tokens', UNSET), temperature=model_settings.get('temperature', UNSET), @@ -266,14 +269,16 @@ async def _stream_completions_create( if model_request_parameters.builtin_tools: raise UserError('Mistral does not support built-in tools') - if model_request_parameters.function_tools: - # Function Calling + tools, tool_choice = self._get_tool_choice(model_request_parameters, model_settings) + + if tools: + # Function Calling mode (with filtered tools) response = await self.client.chat.stream_async( model=str(self._model_name), messages=mistral_messages, n=1, - tools=self._map_function_and_output_tools_definition(model_request_parameters) or UNSET, - tool_choice=self._get_tool_choice(model_request_parameters), + tools=tools, + tool_choice=tool_choice, temperature=model_settings.get('temperature', UNSET), top_p=model_settings.get('top_p', 1), max_tokens=model_settings.get('max_tokens', UNSET), @@ -284,9 +289,9 @@ async def _stream_completions_create( http_headers={'User-Agent': get_user_agent()}, ) - elif model_request_parameters.output_tools: + elif model_request_parameters.output_tools: # pragma: no cover # TODO: Port to native "manual JSON" mode - # Json Mode + # Json Mode (only output tools, no function tools filtered in) parameters_json_schemas = [tool.parameters_json_schema for tool in model_request_parameters.output_tools] user_output_format_message = self._generate_user_output_format(parameters_json_schemas) mistral_messages.append(user_output_format_message) @@ -302,7 +307,7 @@ async def _stream_completions_create( ) else: - # Stream Mode + # Stream Mode (no tools at all) response = await self.client.chat.stream_async( model=str(self._model_name), messages=mistral_messages, @@ -312,37 +317,68 @@ async def _stream_completions_create( assert response, 'A unexpected empty response from Mistral.' return response - def _get_tool_choice(self, model_request_parameters: ModelRequestParameters) -> MistralToolChoiceEnum | None: - """Get tool choice for the model. + def _get_tool_choice( + self, + model_request_parameters: ModelRequestParameters, + model_settings: MistralModelSettings, + ) -> tuple[list[MistralTool] | None, MistralToolChoiceEnum | None]: + """Get tools and tool choice for the model. + + Returns a tuple of (tools, tool_choice): + - tools: List of MistralTool definitions to send, or None if no tools + - tool_choice: "auto", "any", "none", "required", or None + Tool choice semantics: - "auto": Default mode. Model decides if it uses the tool or not. - "any": Select any tool. - "none": Prevents tool use. - "required": Forces tool use. """ - if not model_request_parameters.function_tools and not model_request_parameters.output_tools: - return None - elif not model_request_parameters.allow_text_output: - return 'required' + resolved = _resolve_tool_choice(model_settings, model_request_parameters) + function_tools = model_request_parameters.function_tools + output_tools = model_request_parameters.output_tools + + if resolved is None: + tool_defs_to_send = [*function_tools, *output_tools] else: - return 'auto' + tool_defs_to_send = resolved.filter_tools(function_tools, output_tools) - def _map_function_and_output_tools_definition( - self, model_request_parameters: ModelRequestParameters - ) -> list[MistralTool] | None: - """Map function and output tools to MistralTool format. + if not tool_defs_to_send: + return None, None - Returns None if both function_tools and output_tools are empty. - """ tools = [ MistralTool( function=MistralFunction( name=r.name, parameters=r.parameters_json_schema, description=r.description or '' ) ) - for r in model_request_parameters.tool_defs.values() + for r in tool_defs_to_send ] - return tools if tools else None + + # Determine tool_choice value + if resolved is None: + # Default behavior: infer from allow_text_output + if not model_request_parameters.allow_text_output: + tool_choice: MistralToolChoiceEnum = 'required' + else: + tool_choice = 'auto' + elif resolved.mode == 'auto': + if not model_request_parameters.allow_text_output: + tool_choice = 'required' + else: + tool_choice = 'auto' + elif resolved.mode == 'required': + tool_choice = 'required' + elif resolved.mode == 'none': + # We've filtered to output tools only, use 'required' if there are output tools + tool_choice = 'required' if output_tools else 'none' + elif resolved.mode == 'specific': + # Mistral doesn't support specific tool forcing, use 'required' + tool_choice = 'required' + else: + assert_never(resolved.mode) + + return tools, tool_choice def _process_response(self, response: MistralChatCompletionResponse) -> ModelResponse: """Process a non-streamed response, and prepare a message to return.""" diff --git a/pydantic_ai_slim/pydantic_ai/models/openai.py b/pydantic_ai_slim/pydantic_ai/models/openai.py index 61c8df132b..dda658f1f7 100644 --- a/pydantic_ai_slim/pydantic_ai/models/openai.py +++ b/pydantic_ai_slim/pydantic_ai/models/openai.py @@ -52,7 +52,15 @@ from ..providers import Provider, infer_provider from ..settings import ModelSettings from ..tools import ToolDefinition -from . import Model, ModelRequestParameters, StreamedResponse, check_allow_model_requests, download_item, get_user_agent +from . import ( + Model, + ModelRequestParameters, + StreamedResponse, + _resolve_tool_choice, # pyright: ignore[reportPrivateUsage] + check_allow_model_requests, + download_item, + get_user_agent, +) try: from openai import NOT_GIVEN, APIConnectionError, APIStatusError, AsyncOpenAI, AsyncStream @@ -67,6 +75,8 @@ chat_completion_chunk, chat_completion_token_logprob, ) + from openai.types.chat.chat_completion_allowed_tool_choice_param import ChatCompletionAllowedToolChoiceParam + from openai.types.chat.chat_completion_allowed_tools_param import ChatCompletionAllowedToolsParam from openai.types.chat.chat_completion_content_part_image_param import ImageURL from openai.types.chat.chat_completion_content_part_input_audio_param import InputAudio from openai.types.chat.chat_completion_content_part_param import File, FileFile @@ -75,19 +85,24 @@ from openai.types.chat.chat_completion_message_function_tool_call_param import ( ChatCompletionMessageFunctionToolCallParam, ) + from openai.types.chat.chat_completion_named_tool_choice_param import ChatCompletionNamedToolChoiceParam from openai.types.chat.chat_completion_prediction_content_param import ChatCompletionPredictionContentParam + from openai.types.chat.chat_completion_tool_choice_option_param import ChatCompletionToolChoiceOptionParam from openai.types.chat.completion_create_params import ( WebSearchOptions, WebSearchOptionsUserLocation, WebSearchOptionsUserLocationApproximate, ) from openai.types.responses import ComputerToolParam, FileSearchToolParam, WebSearchToolParam + from openai.types.responses.response_create_params import ToolChoice as ResponsesToolChoice from openai.types.responses.response_input_param import FunctionCallOutput, Message from openai.types.responses.response_reasoning_item_param import ( Content as ReasoningContent, Summary as ReasoningSummary, ) from openai.types.responses.response_status import ResponseStatus + from openai.types.responses.tool_choice_allowed_param import ToolChoiceAllowedParam + from openai.types.responses.tool_choice_function_param import ToolChoiceFunctionParam from openai.types.shared import ReasoningEffort from openai.types.shared_params import Reasoning except ImportError as _import_error: @@ -535,19 +550,9 @@ async def _completions_create( model_settings: OpenAIChatModelSettings, model_request_parameters: ModelRequestParameters, ) -> chat.ChatCompletion | AsyncStream[ChatCompletionChunk]: - tools = self._get_tools(model_request_parameters) + tools, tool_choice = self._get_tool_choice(model_settings, model_request_parameters) web_search_options = self._get_web_search_options(model_request_parameters) - if not tools: - tool_choice: Literal['none', 'required', 'auto'] | None = None - elif ( - not model_request_parameters.allow_text_output - and OpenAIModelProfile.from_profile(self.profile).openai_supports_tool_choice_required - ): - tool_choice = 'required' - else: - tool_choice = 'auto' - openai_messages = await self._map_messages(messages, model_request_parameters) response_format: chat.completion_create_params.ResponseFormat | None = None @@ -743,8 +748,93 @@ def _streamed_response_cls(self) -> type[OpenAIStreamedResponse]: def _map_usage(self, response: chat.ChatCompletion) -> usage.RequestUsage: return _map_usage(response, self._provider.name, self._provider.base_url, self.model_name) - def _get_tools(self, model_request_parameters: ModelRequestParameters) -> list[chat.ChatCompletionToolParam]: - return [self._map_tool_definition(r) for r in model_request_parameters.tool_defs.values()] + def _get_tool_choice( + self, + model_settings: OpenAIChatModelSettings, + model_request_parameters: ModelRequestParameters, + ) -> tuple[list[chat.ChatCompletionToolParam], ChatCompletionToolChoiceOptionParam | None]: + """Determine which tools to send and the API tool_choice value. + + Returns: + A tuple of (filtered_tools, tool_choice). + """ + function_tools = model_request_parameters.function_tools + output_tools = model_request_parameters.output_tools + openai_profile = OpenAIModelProfile.from_profile(self.profile) + + resolved = _resolve_tool_choice(model_settings, model_request_parameters) + + if resolved is None: + tool_defs_to_send = [*function_tools, *output_tools] + else: + tool_defs_to_send = resolved.filter_tools(function_tools, output_tools) + + tools: list[chat.ChatCompletionToolParam] = [self._map_tool_definition(t) for t in tool_defs_to_send] + + if not tools: + return tools, None + + tool_choice: ChatCompletionToolChoiceOptionParam + + if resolved is None: + if not model_request_parameters.allow_text_output and openai_profile.openai_supports_tool_choice_required: + tool_choice = 'required' + else: + tool_choice = 'auto' + + elif resolved.mode == 'auto': + if not model_request_parameters.allow_text_output and openai_profile.openai_supports_tool_choice_required: + tool_choice = 'required' + else: + tool_choice = 'auto' + + elif resolved.mode == 'required': + if openai_profile.openai_supports_tool_choice_required: + tool_choice = 'required' + else: + warnings.warn( + f"tool_choice='required' is not supported by model {self.model_name!r}, falling back to 'auto'", + UserWarning, + stacklevel=6, + ) + tool_choice = 'auto' + + elif resolved.mode == 'none': + if len(output_tools) == 1: + tool_choice = ChatCompletionNamedToolChoiceParam( + type='function', function={'name': output_tools[0].name} + ) + else: + allowed_mode: Literal['auto', 'required'] = ( + 'required' if not model_request_parameters.allow_text_output else 'auto' + ) + tool_choice = ChatCompletionAllowedToolChoiceParam( + type='allowed_tools', + allowed_tools=ChatCompletionAllowedToolsParam( + mode=allowed_mode, + tools=[{'type': 'function', 'function': {'name': t.name}} for t in output_tools], + ), + ) + + elif resolved.mode == 'specific': + if len(resolved.tool_names) == 1: + tool_choice = ChatCompletionNamedToolChoiceParam( + type='function', function={'name': resolved.tool_names[0]} + ) + else: + allowed_mode = 'required' if not model_request_parameters.allow_text_output else 'auto' + tool_choice = ChatCompletionAllowedToolChoiceParam( + type='allowed_tools', + allowed_tools=ChatCompletionAllowedToolsParam( + mode=allowed_mode, + tools=[{'type': 'function', 'function': {'name': n}} for n in resolved.tool_names], + ), + ) + + else: + assert_never(resolved.mode) + + return tools, tool_choice def _get_web_search_options(self, model_request_parameters: ModelRequestParameters) -> WebSearchOptions | None: for tool in model_request_parameters.builtin_tools: @@ -1340,25 +1430,20 @@ async def _responses_create( model_request_parameters: ModelRequestParameters, ) -> AsyncStream[responses.ResponseStreamEvent]: ... - async def _responses_create( # noqa: C901 + async def _responses_create( self, messages: list[ModelRequest | ModelResponse], stream: bool, model_settings: OpenAIResponsesModelSettings, model_request_parameters: ModelRequestParameters, ) -> responses.Response | AsyncStream[responses.ResponseStreamEvent]: - tools = ( + function_tools, tool_choice = self._get_responses_tool_choice(model_settings, model_request_parameters) + tools: list[responses.ToolParam] = ( self._get_builtin_tools(model_request_parameters) + list(model_settings.get('openai_builtin_tools', [])) - + self._get_tools(model_request_parameters) + + function_tools ) profile = OpenAIModelProfile.from_profile(self.profile) - if not tools: - tool_choice: Literal['none', 'required', 'auto'] | None = None - elif not model_request_parameters.allow_text_output and profile.openai_supports_tool_choice_required: - tool_choice = 'required' - else: - tool_choice = 'auto' previous_response_id = model_settings.get('openai_previous_response_id') if previous_response_id == 'auto': @@ -1468,8 +1553,82 @@ def _get_reasoning(self, model_settings: OpenAIResponsesModelSettings) -> Reason return OMIT return Reasoning(effort=reasoning_effort, summary=reasoning_summary) - def _get_tools(self, model_request_parameters: ModelRequestParameters) -> list[responses.FunctionToolParam]: - return [self._map_tool_definition(r) for r in model_request_parameters.tool_defs.values()] + def _get_responses_tool_choice( + self, + model_settings: OpenAIResponsesModelSettings, + model_request_parameters: ModelRequestParameters, + ) -> tuple[list[responses.FunctionToolParam], ResponsesToolChoice | None]: + """Determine which tools to send and the API tool_choice value. + + Returns: + A tuple of (filtered_function_tools, tool_choice). + Note: builtin tools are handled separately and should be added to this list. + """ + function_tools = model_request_parameters.function_tools + output_tools = model_request_parameters.output_tools + openai_profile = OpenAIModelProfile.from_profile(self.profile) + + resolved = _resolve_tool_choice(model_settings, model_request_parameters) + + if resolved is None: + tool_defs_to_send = [*function_tools, *output_tools] + else: + tool_defs_to_send = resolved.filter_tools(function_tools, output_tools) + tools: list[responses.FunctionToolParam] = [self._map_tool_definition(t) for t in tool_defs_to_send] + + tool_choice: ResponsesToolChoice | None + + if resolved is None: + if not model_request_parameters.allow_text_output and openai_profile.openai_supports_tool_choice_required: + tool_choice = 'required' + else: + tool_choice = 'auto' + + elif resolved.mode == 'auto': + if not model_request_parameters.allow_text_output and openai_profile.openai_supports_tool_choice_required: + tool_choice = 'required' + else: + tool_choice = 'auto' + + elif resolved.mode == 'required': + if openai_profile.openai_supports_tool_choice_required: + tool_choice = 'required' + else: + warnings.warn( + f"tool_choice='required' is not supported by model {self.model_name!r}, falling back to 'auto'", + UserWarning, + stacklevel=6, + ) + tool_choice = 'auto' + + elif resolved.mode == 'none': + if len(output_tools) == 1: + tool_choice = ToolChoiceFunctionParam(type='function', name=output_tools[0].name) + else: + allowed_mode: Literal['auto', 'required'] = ( + 'required' if not model_request_parameters.allow_text_output else 'auto' + ) + tool_choice = ToolChoiceAllowedParam( + type='allowed_tools', + mode=allowed_mode, + tools=[{'type': 'function', 'name': t.name} for t in output_tools], + ) + + elif resolved.mode == 'specific': + if len(resolved.tool_names) == 1: + tool_choice = ToolChoiceFunctionParam(type='function', name=resolved.tool_names[0]) + else: + allowed_mode = 'required' if not model_request_parameters.allow_text_output else 'auto' + tool_choice = ToolChoiceAllowedParam( + type='allowed_tools', + mode=allowed_mode, + tools=[{'type': 'function', 'name': n} for n in resolved.tool_names], + ) + + else: + assert_never(resolved.mode) + + return tools, tool_choice def _get_builtin_tools(self, model_request_parameters: ModelRequestParameters) -> list[responses.ToolParam]: tools: list[responses.ToolParam] = [] diff --git a/pydantic_ai_slim/pydantic_ai/settings.py b/pydantic_ai_slim/pydantic_ai/settings.py index 6941eb1ab3..dabd4b0333 100644 --- a/pydantic_ai_slim/pydantic_ai/settings.py +++ b/pydantic_ai_slim/pydantic_ai/settings.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Literal + from httpx import Timeout from typing_extensions import TypedDict @@ -7,8 +9,10 @@ class ModelSettings(TypedDict, total=False): """Settings to configure an LLM. - Here we include only settings which apply to multiple models / model providers, + Includes only settings which apply to multiple models / model providers, though not all of these settings are supported by all models. + + All types must be JSON-serializable. """ max_tokens: int @@ -88,6 +92,30 @@ class ModelSettings(TypedDict, total=False): * Anthropic """ + tool_choice: Literal['none', 'required', 'auto'] | list[str] | None + """Control which function tools the model can use. + + This setting affects both which tools are sent to the model and the API tool_choice parameter. + Output tools (used for structured output) are managed separately and remain available when needed. + + * `None` (default): All tools sent, tool_choice determined by output configuration + * `'auto'`: All tools sent, model decides whether to use them + * `'required'`: Only function tools sent (no output tools), model must use one + * `'none'`: Only output tools sent (no function tools), or no tools if none needed + * `list[str]`: Only specified function tools sent (plus output tools), model must use one + * `[]` (empty list): Treated as `'none'` + + Supported by: + + * OpenAI + * Anthropic (note: `'required'` and specific tools not supported with thinking/extended thinking) + * Gemini + * Groq + * Mistral + * HuggingFace + * Bedrock + """ + seed: int """The random seed to use for the model, theoretically allowing for deterministic results. diff --git a/tests/models/anthropic/cassettes/test_tool_choice/test_tool_choice_auto_live.yaml b/tests/models/anthropic/cassettes/test_tool_choice/test_tool_choice_auto_live.yaml new file mode 100644 index 0000000000..66e668551b --- /dev/null +++ b/tests/models/anthropic/cassettes/test_tool_choice/test_tool_choice_auto_live.yaml @@ -0,0 +1,79 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '369' + content-type: + - application/json + host: + - api.anthropic.com + method: POST + parsed_body: + max_tokens: 4096 + messages: + - content: + - text: What is 2+2? + type: text + role: user + model: claude-haiku-4-5 + stream: false + tool_choice: + type: auto + tools: + - description: Get the weather for a city. + input_schema: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + name: get_weather + uri: https://api.anthropic.com/v1/messages?beta=true + response: + headers: + connection: + - keep-alive + content-length: + - '521' + content-type: + - application/json + retry-after: + - '14' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + parsed_body: + content: + - text: |- + 2 + 2 = 4 + + This is a simple arithmetic problem. I don't need to use any tools to answer this basic math question. + type: text + id: msg_01MPRAkxA9ciXHJpJwp7Vrd9 + model: claude-haiku-4-5-20251001 + 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: 571 + output_tokens: 36 + service_tier: standard + status: + code: 200 + message: OK +version: 1 diff --git a/tests/models/anthropic/cassettes/test_tool_choice/test_tool_choice_none_live.yaml b/tests/models/anthropic/cassettes/test_tool_choice/test_tool_choice_none_live.yaml new file mode 100644 index 0000000000..ab6a9828b4 --- /dev/null +++ b/tests/models/anthropic/cassettes/test_tool_choice/test_tool_choice_none_live.yaml @@ -0,0 +1,79 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '369' + content-type: + - application/json + host: + - api.anthropic.com + method: POST + parsed_body: + max_tokens: 4096 + messages: + - content: + - text: What is 2+2? + type: text + role: user + model: claude-haiku-4-5 + stream: false + tool_choice: + type: none + tools: + - description: Get the weather for a city. + input_schema: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + name: get_weather + uri: https://api.anthropic.com/v1/messages?beta=true + response: + headers: + connection: + - keep-alive + content-length: + - '480' + content-type: + - application/json + retry-after: + - '45' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + parsed_body: + content: + - text: |- + 2 + 2 = 4 + + This is a simple arithmetic problem. The sum of 2 and 2 is 4. + type: text + id: msg_01SXvsFVTX8YH6EuEmvTgEiW + model: claude-haiku-4-5-20251001 + 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: 571 + output_tokens: 35 + service_tier: standard + status: + code: 200 + message: OK +version: 1 diff --git a/tests/models/anthropic/cassettes/test_tool_choice/test_tool_choice_required_live.yaml b/tests/models/anthropic/cassettes/test_tool_choice/test_tool_choice_required_live.yaml new file mode 100644 index 0000000000..3977646c12 --- /dev/null +++ b/tests/models/anthropic/cassettes/test_tool_choice/test_tool_choice_required_live.yaml @@ -0,0 +1,187 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '640' + content-type: + - application/json + host: + - api.anthropic.com + method: POST + parsed_body: + max_tokens: 4096 + messages: + - content: + - text: What is the temperature in Paris? Use the weather tool and return the temperature as a number. + type: text + role: user + model: claude-haiku-4-5 + stream: false + tool_choice: + type: any + tools: + - description: Get the weather for a city. + input_schema: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + name: get_weather + - description: The final response which ends this conversation + input_schema: + properties: + response: + type: integer + required: + - response + type: object + name: final_result + uri: https://api.anthropic.com/v1/messages?beta=true + response: + headers: + connection: + - keep-alive + content-length: + - '484' + content-type: + - application/json + retry-after: + - '53' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + parsed_body: + content: + - id: toolu_01D3gpU1tfJop7nGMdNfcRsx + input: + city: Paris + name: get_weather + type: tool_use + id: msg_01Fsv9kDABeC5Au7dB6uvE8R + model: claude-haiku-4-5-20251001 + role: assistant + stop_reason: tool_use + 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: 730 + output_tokens: 38 + service_tier: standard + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '945' + content-type: + - application/json + host: + - api.anthropic.com + method: POST + parsed_body: + max_tokens: 4096 + messages: + - content: + - text: What is the temperature in Paris? Use the weather tool and return the temperature as a number. + type: text + role: user + - content: + - id: toolu_01D3gpU1tfJop7nGMdNfcRsx + input: + city: Paris + name: get_weather + type: tool_use + role: assistant + - content: + - content: The weather in Paris is sunny and 72°F. + is_error: false + tool_use_id: toolu_01D3gpU1tfJop7nGMdNfcRsx + type: tool_result + role: user + model: claude-haiku-4-5 + stream: false + tool_choice: + type: any + tools: + - description: Get the weather for a city. + input_schema: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + name: get_weather + - description: The final response which ends this conversation + input_schema: + properties: + response: + type: integer + required: + - response + type: object + name: final_result + uri: https://api.anthropic.com/v1/messages?beta=true + response: + headers: + connection: + - keep-alive + content-length: + - '484' + content-type: + - application/json + retry-after: + - '50' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + parsed_body: + content: + - id: toolu_01Q9UEiNVmpbMtSxtML7zdMo + input: + response: 72 + name: final_result + type: tool_use + id: msg_01AmQc9gZFMuunfaLmVoF5T5 + model: claude-haiku-4-5-20251001 + role: assistant + stop_reason: tool_use + 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: 806 + output_tokens: 38 + service_tier: standard + status: + code: 200 + message: OK +version: 1 diff --git a/tests/models/anthropic/cassettes/test_tool_choice/test_tool_choice_specific_live.yaml b/tests/models/anthropic/cassettes/test_tool_choice/test_tool_choice_specific_live.yaml new file mode 100644 index 0000000000..24d4832a82 --- /dev/null +++ b/tests/models/anthropic/cassettes/test_tool_choice/test_tool_choice_specific_live.yaml @@ -0,0 +1,191 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '580' + content-type: + - application/json + host: + - api.anthropic.com + method: POST + parsed_body: + max_tokens: 4096 + messages: + - content: + - text: What is 2+2? + type: text + role: user + model: claude-haiku-4-5 + stream: false + tool_choice: + name: get_weather + type: tool + tools: + - description: Get the weather for a city. + input_schema: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + name: get_weather + - description: Get the current time in a city. + input_schema: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + name: get_time + uri: https://api.anthropic.com/v1/messages?beta=true + response: + headers: + connection: + - keep-alive + content-length: + - '488' + content-type: + - application/json + retry-after: + - '47' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + parsed_body: + content: + - id: toolu_01GMZox9yeNRGPMasr4G2sb8 + input: + city: + name: get_weather + type: tool_use + id: msg_01DPYqAxNvskqyAt1TodvqCf + model: claude-haiku-4-5-20251001 + role: assistant + stop_reason: tool_use + 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: 730 + output_tokens: 34 + service_tier: standard + status: + code: 200 + message: OK +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '893' + content-type: + - application/json + host: + - api.anthropic.com + method: POST + parsed_body: + max_tokens: 4096 + messages: + - content: + - text: What is 2+2? + type: text + role: user + - content: + - id: toolu_01GMZox9yeNRGPMasr4G2sb8 + input: + city: + name: get_weather + type: tool_use + role: assistant + - content: + - content: The weather in is sunny and 72°F. + is_error: false + tool_use_id: toolu_01GMZox9yeNRGPMasr4G2sb8 + type: tool_result + role: user + model: claude-haiku-4-5 + stream: false + tool_choice: + name: get_weather + type: tool + tools: + - description: Get the weather for a city. + input_schema: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + name: get_weather + - description: Get the current time in a city. + input_schema: + additionalProperties: false + properties: + city: + type: string + required: + - city + type: object + name: get_time + uri: https://api.anthropic.com/v1/messages?beta=true + response: + headers: + connection: + - keep-alive + content-length: + - '488' + content-type: + - application/json + retry-after: + - '44' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + parsed_body: + content: + - id: toolu_01AuG2J1q864E4YSXa6HUUsh + input: + city: + name: get_weather + type: tool_use + id: msg_012REmAsZn5D4TYHohRmPDGV + model: claude-haiku-4-5-20251001 + role: assistant + stop_reason: tool_use + 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: 810 + output_tokens: 34 + service_tier: standard + status: + code: 200 + message: OK +version: 1 diff --git a/tests/models/anthropic/test_tool_choice.py b/tests/models/anthropic/test_tool_choice.py new file mode 100644 index 0000000000..be9796d788 --- /dev/null +++ b/tests/models/anthropic/test_tool_choice.py @@ -0,0 +1,97 @@ +"""Tests for Anthropic tool_choice setting.""" + +from __future__ import annotations as _annotations + +import pytest + +from pydantic_ai import Agent + +from ...conftest import try_import + +with try_import() as imports_successful: + from pydantic_ai.models.anthropic import AnthropicModel + from pydantic_ai.providers.anthropic import AnthropicProvider + +pytestmark = [ + pytest.mark.skipif(not imports_successful(), reason='anthropic not installed'), + pytest.mark.anyio, + pytest.mark.vcr, +] + + +async def test_tool_choice_auto_live(allow_model_requests: None, anthropic_api_key: str): + """Test tool_choice='auto' allows model to decide whether to use tools.""" + m = AnthropicModel('claude-haiku-4-5', provider=AnthropicProvider(api_key=anthropic_api_key)) + agent = Agent(m) + + @agent.tool_plain + def get_weather(city: str) -> str: + """Get the weather for a city.""" + return f'The weather in {city} is sunny and 72°F.' # pragma: no cover + + result = await agent.run('What is 2+2?', model_settings={'tool_choice': 'auto'}) + assert result.output is not None + assert '4' in result.output + + +async def test_tool_choice_required_live(allow_model_requests: None, anthropic_api_key: str): + """Test tool_choice='required' forces model to use a tool.""" + m = AnthropicModel('claude-haiku-4-5', provider=AnthropicProvider(api_key=anthropic_api_key)) + agent = Agent(m) + + @agent.tool_plain + def get_weather(city: str) -> str: + """Get the weather for a city.""" + return f'The weather in {city} is sunny and 72°F.' + + result = await agent.run( + 'What is the temperature in Paris? Use the weather tool and return the temperature as a number.', + output_type=int, + model_settings={'tool_choice': 'required'}, + ) + assert result.output == 72 + + +async def test_tool_choice_none_live(allow_model_requests: None, anthropic_api_key: str): + """Test tool_choice='none' prevents model from using function tools.""" + m = AnthropicModel('claude-haiku-4-5', provider=AnthropicProvider(api_key=anthropic_api_key)) + agent = Agent(m) + + @agent.tool_plain + def get_weather(city: str) -> str: + """Get the weather for a city.""" + return f'The weather in {city} is sunny and 72°F.' # pragma: no cover + + result = await agent.run( + 'What is 2+2?', + model_settings={'tool_choice': 'none'}, + ) + assert result.output is not None + assert '4' in result.output + + +async def test_tool_choice_specific_live(allow_model_requests: None, anthropic_api_key: str): + """Test tool_choice=['tool_name'] forces model to use the named tool.""" + from pydantic_ai import UsageLimits + from pydantic_ai.exceptions import UsageLimitExceeded + + m = AnthropicModel('claude-haiku-4-5', provider=AnthropicProvider(api_key=anthropic_api_key)) + agent = Agent(m) + + @agent.tool_plain + def get_weather(city: str) -> str: + """Get the weather for a city.""" + return f'The weather in {city} is sunny and 72°F.' + + @agent.tool_plain + def get_time(city: str) -> str: + """Get the current time in a city.""" + return f'The time in {city} is 3:00 PM.' # pragma: no cover + + # Agent is forced to call get_weather even though it's unrelated to the prompt + with pytest.raises(UsageLimitExceeded): + await agent.run( + 'What is 2+2?', + model_settings={'tool_choice': ['get_weather']}, + usage_limits=UsageLimits(request_limit=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 index 2992be928b..f25007369b 100644 --- a/tests/models/cassettes/test_anthropic/test_anthropic_web_fetch_tool.yaml +++ b/tests/models/cassettes/test_anthropic/test_anthropic_web_fetch_tool.yaml @@ -42,24 +42,23 @@ interactions: connection: - keep-alive content-length: - - '21807' + - '34284' content-type: - application/json retry-after: - - '39' + - '52' 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. + - signature: EooDCkYIChgCKkBe6ddqCwT1CKptPg14EIIaDdq/qCKZucUmb9w4ci/O5pSzheW3Vv33pkeqmFPrcr4l5CWAVM/xaV7NjHNwOPohEgza8xxqBGZ7jds5BzMaDFyd3dP3X5SSgPSg7SIwfyJLZ+yYCfCrpsgV77T7Ay6iBvfLp0ZaSb+ph+2YkSSVAxFDodWxWyv5DiVHRLJnKvEBcESvFzbas8bML22jelhD1nxGiyr6GGBCjavRGaz6LpPExc03FEa9qS0YEvAniVkVxLJprdDcW4Sm2/lLTh6TRiiicyELomVYxI4FrK1R981pA9ZdDgP8iE3YmcvhvvVJpxgUK+6Tr0LJSq5GgnlcPVEcXc+PYzqLNwlHiH+Vm9bPVs/N/JhfYBPKKREWgtXQBvpsxDM8f5Rj6569D7K3oKZ7Dbglc8eWyn1KFYHV4R+sXc4XOHJDPWuEjgW5/aYUfnP5yDlscfTHDgfrZVy7se8rjgtnpwLNstk7GbLUzgIft6p7hTTUIGB7icopfZtoahgB + thinking: The user wants me to fetch the content from https://ai.pydantic.dev and return only the first sentence. + I need to use the web_fetch tool to get the content from this URL, then identify the first sentence and return only + that. type: thinking - - id: srvtoolu_01So85wNUocinTvFfgKCfQeb + - id: srvtoolu_01MSZmtbzt6NmQizTETf3GPF input: url: https://ai.pydantic.dev name: web_fetch @@ -67,300 +66,291 @@ interactions: - 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. + data: "Pydantic AI\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n \n\n\n\n\n\n[Skip to content](#pydantic-ai)\n\n**[Pydantic + AI Gateway](/gateway) is now available! \U0001F680\nEnterprise-ready AI model routing: One key for all your + models with real-time monitoring and budget control that works.**\n\n[![logo](img/logo-white.svg)](. \"Pydantic + AI\")\n\n\n\n\nPydantic AI\n\nPydantic AI\n\n\n\n\n\n\n\nType to start searching\n\n[pydantic/pydantic-ai](https://github.com/pydantic/pydantic-ai + \"Go to repository\")\n\n[![logo](img/logo-white.svg)](. \"Pydantic AI\")\nPydantic AI\n\n[pydantic/pydantic-ai](https://github.com/pydantic/pydantic-ai + \"Go to repository\")\n\n* Pydantic AI\n\n [Pydantic AI](.)\n\n\n\n Table of contents\n + [Why use Pydantic + AI](#why-use-pydantic-ai)\n + [Hello World Example](#hello-world-example)\n + [Tools & Dependency Injection + Example](#tools-dependency-injection-example)\n + [Instrumentation with Pydantic Logfire](#instrumentation-with-pydantic-logfire)\n + \ + [llms.txt](#llmstxt)\n + [Next Steps](#next-steps)\n* [Installation](install/)\n* [Getting Help](help/)\n* + [Troubleshooting](troubleshooting/)\n* [Pydantic AI Gateway](gateway/)\n* Documentation\n\n\n\n\n Documentation\n + \ + Core Concepts\n\n\n\n\n Core Concepts\n - [Agents](agents/)\n - [Dependencies](dependencies/)\n + \ - [Function Tools](tools/)\n - [Output](output/)\n - [Messages and chat history](message-history/)\n + \ - [Direct Model Requests](direct/)\n + Models & Providers\n\n\n\n\n Models & Providers\n - [Overview](models/overview/)\n + \ - [OpenAI](models/openai/)\n - [Anthropic](models/anthropic/)\n - [Google](models/google/)\n - + [Bedrock](models/bedrock/)\n - [Cohere](models/cohere/)\n - [Groq](models/groq/)\n - [Hugging Face](models/huggingface/)\n + \ - [Mistral](models/mistral/)\n - [OpenRouter](models/openrouter/)\n - [Outlines](models/outlines/)\n + \ + Tools & Toolsets\n\n\n\n\n Tools & Toolsets\n - [Function Tools](tools/)\n - [Advanced Tool Features](tools-advanced/)\n + \ - [Toolsets](toolsets/)\n - [Deferred Tools](deferred-tools/)\n - [Built-in Tools](builtin-tools/)\n + \ - [Common Tools](common-tools/)\n - [Third-Party Tools](third-party-tools/)\n + Advanced Features\n\n\n\n\n + \ Advanced Features\n - [Image, Audio, Video & Document Input](input/)\n - [Thinking](thinking/)\n + \ - [HTTP Request Retries](retries/)\n + MCP\n\n\n\n\n MCP\n - [Overview](mcp/overview/)\n - [Client](mcp/client/)\n + \ - [FastMCP Client](mcp/fastmcp-client/)\n - [Server](mcp/server/)\n + [Multi-Agent Patterns](multi-agent-applications/)\n + \ + [Testing](testing/)\n* Pydantic Evals\n\n\n\n\n Pydantic Evals\n + [Overview](evals/)\n + Getting Started\n\n\n\n\n + \ Getting Started\n - [Quick Start](evals/quick-start/)\n - [Core Concepts](evals/core-concepts/)\n + \ + Evaluators\n\n\n\n\n Evaluators\n - [Overview](evals/evaluators/overview/)\n - [Built-in Evaluators](evals/evaluators/built-in/)\n + \ - [LLM Judge](evals/evaluators/llm-judge/)\n - [Custom Evaluators](evals/evaluators/custom/)\n - + [Span-Based](evals/evaluators/span-based/)\n + How-To Guides\n\n\n\n\n How-To Guides\n - [Logfire Integration](evals/how-to/logfire-integration/)\n + \ - [Dataset Management](evals/how-to/dataset-management/)\n - [Dataset Serialization](evals/how-to/dataset-serialization/)\n + \ - [Concurrency & Performance](evals/how-to/concurrency/)\n - [Retry Strategies](evals/how-to/retry-strategies/)\n + \ - [Metrics & Attributes](evals/how-to/metrics-attributes/)\n + Examples\n\n\n\n\n Examples\n - + [Simple Validation](evals/examples/simple-validation/)\n* Pydantic Graph\n\n\n\n\n Pydantic Graph\n + [Overview](graph/)\n + \ + [Beta API](graph/beta/)\n\n Beta API\n - [Steps](graph/beta/steps/)\n - [Joins & Reducers](graph/beta/joins/)\n + \ - [Decisions](graph/beta/decisions/)\n - [Parallel Execution](graph/beta/parallel/)\n* Integrations\n\n\n\n\n + \ Integrations\n + [Debugging & Monitoring with Pydantic Logfire](logfire/)\n + Durable Execution\n\n\n\n\n + \ Durable Execution\n - [Overview](durable_execution/overview/)\n - [Temporal](durable_execution/temporal/)\n + \ - [DBOS](durable_execution/dbos/)\n - [Prefect](durable_execution/prefect/)\n + UI Event Streams\n\n\n\n\n + \ UI Event Streams\n - [Overview](ui/overview/)\n - [AG-UI](ui/ag-ui/)\n - [Vercel AI](ui/vercel-ai/)\n + \ + [Agent2Agent (A2A)](a2a/)\n* Related Packages\n\n\n\n\n Related Packages\n + [Clai](cli/)\n* Examples\n\n\n\n\n + \ Examples\n + [Setup](examples/setup/)\n + Getting Started\n\n\n\n\n Getting Started\n - [Pydantic + Model](examples/pydantic-model/)\n - [Weather agent](examples/weather-agent/)\n + Conversational Agents\n\n\n\n\n + \ Conversational Agents\n - [Chat App with FastAPI](examples/chat-app/)\n - [Bank support](examples/bank-support/)\n + \ + Data & Analytics\n\n\n\n\n Data & Analytics\n - [SQL Generation](examples/sql-gen/)\n - [Data + Analyst](examples/data-analyst/)\n - [RAG](examples/rag/)\n + Streaming\n\n\n\n\n Streaming\n - + [Stream markdown](examples/stream-markdown/)\n - [Stream whales](examples/stream-whales/)\n + Complex + Workflows\n\n\n\n\n Complex Workflows\n - [Flight booking](examples/flight-booking/)\n - [Question + Graph](examples/question-graph/)\n + Business Applications\n\n\n\n\n Business Applications\n - [Slack + Lead Qualifier with Modal](examples/slack-lead-qualifier/)\n + UI Examples\n\n\n\n\n UI Examples\n - + [Agent User Interaction (AG-UI)](examples/ag-ui/)\n* API Reference\n\n\n\n\n API Reference\n + pydantic\\_ai\n\n\n\n\n + \ pydantic\\_ai\n - [pydantic\\_ai.agent](api/agent/)\n - [pydantic\\_ai.tools](api/tools/)\n - + [pydantic\\_ai.toolsets](api/toolsets/)\n - [pydantic\\_ai.builtin\\_tools](api/builtin_tools/)\n - + [pydantic\\_ai.common\\_tools](api/common_tools/)\n - [pydantic\\_ai.durable\\_exec](api/durable_exec/)\n + \ - [pydantic\\_ai.output](api/output/)\n - [pydantic\\_ai.result](api/result/)\n - [pydantic\\_ai.messages](api/messages/)\n + \ - [pydantic\\_ai.exceptions](api/exceptions/)\n - [pydantic\\_ai.settings](api/settings/)\n - [pydantic\\_ai.usage](api/usage/)\n + \ - [pydantic\\_ai.mcp](api/mcp/)\n - [pydantic\\_ai.format\\_prompt](api/format_prompt/)\n - [pydantic\\_ai.direct](api/direct/)\n + \ - [pydantic\\_ai.ext](api/ext/)\n - [pydantic\\_ai.models.anthropic](api/models/anthropic/)\n - + [pydantic\\_ai.models](api/models/base/)\n - [pydantic\\_ai.models.bedrock](api/models/bedrock/)\n - + [pydantic\\_ai.models.cohere](api/models/cohere/)\n - [pydantic\\_ai.models.fallback](api/models/fallback/)\n + \ - [pydantic\\_ai.models.function](api/models/function/)\n - [pydantic\\_ai.models.google](api/models/google/)\n + \ - [pydantic\\_ai.models.groq](api/models/groq/)\n - [pydantic\\_ai.models.huggingface](api/models/huggingface/)\n + \ - [pydantic\\_ai.models.instrumented](api/models/instrumented/)\n - [pydantic\\_ai.models.mcp\\_sampling](api/models/mcp-sampling/)\n + \ - [pydantic\\_ai.models.mistral](api/models/mistral/)\n - [pydantic\\_ai.models.openai](api/models/openai/)\n + \ - [pydantic\\_ai.models.openrouter](api/models/openrouter/)\n - [pydantic\\_ai.models.outlines](api/models/outlines/)\n + \ - [pydantic\\_ai.models.test](api/models/test/)\n - [pydantic\\_ai.models.wrapper](api/models/wrapper/)\n + \ - [pydantic\\_ai.profiles](api/profiles/)\n - [pydantic\\_ai.providers](api/providers/)\n - [pydantic\\_ai.retries](api/retries/)\n + \ - [pydantic\\_ai.run](api/run/)\n - [pydantic\\_ai.ag\\_ui](api/ag_ui/)\n - [pydantic\\_ai.ui](api/ui/base/)\n + \ - [pydantic\\_ai.ui.ag\\_ui](api/ui/ag_ui/)\n - [pydantic\\_ai.ui.vercel\\_ai](api/ui/vercel_ai/)\n + \ + pydantic\\_evals\n\n\n\n\n pydantic\\_evals\n - [pydantic\\_evals.dataset](api/pydantic_evals/dataset/)\n + \ - [pydantic\\_evals.evaluators](api/pydantic_evals/evaluators/)\n - [pydantic\\_evals.reporting](api/pydantic_evals/reporting/)\n + \ - [pydantic\\_evals.otel](api/pydantic_evals/otel/)\n - [pydantic\\_evals.generation](api/pydantic_evals/generation/)\n + \ + pydantic\\_graph\n\n\n\n\n pydantic\\_graph\n - [pydantic\\_graph](api/pydantic_graph/graph/)\n + \ - [pydantic\\_graph.nodes](api/pydantic_graph/nodes/)\n - [pydantic\\_graph.persistence](api/pydantic_graph/persistence/)\n + \ - [pydantic\\_graph.mermaid](api/pydantic_graph/mermaid/)\n - [pydantic\\_graph.exceptions](api/pydantic_graph/exceptions/)\n + \ - Beta API\n\n\n\n\n Beta API\n * [pydantic\\_graph.beta](api/pydantic_graph/beta/)\n * + [pydantic\\_graph.beta.graph](api/pydantic_graph/beta_graph/)\n * [pydantic\\_graph.beta.graph\\_builder](api/pydantic_graph/beta_graph_builder/)\n + \ * [pydantic\\_graph.beta.step](api/pydantic_graph/beta_step/)\n * [pydantic\\_graph.beta.join](api/pydantic_graph/beta_join/)\n + \ * [pydantic\\_graph.beta.decision](api/pydantic_graph/beta_decision/)\n * [pydantic\\_graph.beta.node](api/pydantic_graph/beta_node/)\n + \ + fasta2a\n\n\n\n\n fasta2a\n - [fasta2a](api/fasta2a/)\n* Project\n\n\n\n\n Project\n + [Contributing](contributing/)\n + \ + [Upgrade Guide](changelog/)\n + [Version policy](version-policy/)\n\nTable of contents\n\n* [Why use + Pydantic AI](#why-use-pydantic-ai)\n* [Hello World Example](#hello-world-example)\n* [Tools & Dependency Injection + Example](#tools-dependency-injection-example)\n* [Instrumentation with Pydantic Logfire](#instrumentation-with-pydantic-logfire)\n* + [llms.txt](#llmstxt)\n* [Next Steps](#next-steps)\n\n# Pydantic AI\n\n![Pydantic AI](./img/pydantic-ai-dark.svg#only-dark)\n\n![Pydantic + AI](./img/pydantic-ai-light.svg#only-light)\n\n*GenAI Agent Framework, the Pydantic way*\n\n[![CI](https://github.com/pydantic/pydantic-ai/actions/workflows/ci.yml/badge.svg?event=push)](https://github.com/pydantic/pydantic-ai/actions/workflows/ci.yml?query=branch%3Amain)\n[![Coverage](https://coverage-badge.samuelcolvin.workers.dev/pydantic/pydantic-ai.svg)](https://coverage-badge.samuelcolvin.workers.dev/redirect/pydantic/pydantic-ai)\n[![PyPI](https://img.shields.io/pypi/v/pydantic-ai.svg)](https://pypi.python.org/pypi/pydantic-ai)\n[![versions](https://img.shields.io/pypi/pyversions/pydantic-ai.svg)](https://github.com/pydantic/pydantic-ai)\n[![license](https://img.shields.io/github/license/pydantic/pydantic-ai.svg)](https://github.com/pydantic/pydantic-ai/blob/main/LICENSE)\n[![Join + Slack](https://img.shields.io/badge/Slack-Join%20Slack-4A154B?logo=slack)](https://logfire.pydantic.dev/docs/join-slack/)\n\nPydantic + AI is a Python agent framework designed to help you\nquickly, confidently, and painlessly build production + grade applications and workflows with Generative AI.\n\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.\n\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.\n\nWe built Pydantic AI with one simple aim: to bring + that FastAPI feeling to GenAI app and agent development.\n\n## Why use Pydantic AI\n\n1. **Built 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?* ![\U0001F603](https://cdn.jsdelivr.net/gh/jdecked/twemoji@15.1.0/assets/svg/1f603.svg + \":smiley:\")\n2. **Model-agnostic**:\n 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).\n3. **Seamless Observability**:\n 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).\n4. + **Fully Type-safe**:\n 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.\n5. **Powerful Evals**:\n 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.\n6. **MCP, A2A, and UI**:\n 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.\n7. **Human-in-the-Loop Tool Approval**:\n 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.\n8. **Durable Execution**:\n 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.\n9. **Streamed Outputs**:\n Provides the ability to [stream](output/#streamed-results) + structured output continuously, with immediate validation, ensuring real time access to generated data.\n10. + **Graph Support**:\n 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.\n\nRealistically though, no list is + going to be as convincing as [giving it a try](#next-steps) and seeing how it makes you feel!\n\n**Sign up + for our newsletter, *The Pydantic Stack*, with updates & tutorials on Pydantic AI, Logfire, and Pydantic:**\n\nSubscribe\n\n## + Hello World Example\n\nHere's a minimal example of Pydantic AI:\n\nWith Pydantic AI GatewayDirectly to Provider + API\n\n[Learn about Gateway](../gateway) hello\\_world.py\n\n```\nfrom pydantic_ai import Agent\n\nagent = + Agent( # (1)!\n 'gateway/anthropic:claude-sonnet-4-0',\n instructions='Be concise, reply with one sentence.', + \ # (2)!\n)\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```\n\n1. + 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.\n2. Register static [instructions](agents/#instructions) using a keyword + argument to the agent.\n3. [Run the agent](agents/#running-agents) synchronously, starting a conversation + with the LLM.\n\nhello\\_world.py\n\n```\nfrom pydantic_ai import Agent\n\nagent = Agent( # (1)!\n 'anthropic:claude-sonnet-4-0',\n + \ instructions='Be concise, reply with one sentence.', # (2)!\n)\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```\n\n1. 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.\n2. Register static [instructions](agents/#instructions) using a keyword argument to the agent.\n3. + [Run the agent](agents/#running-agents) synchronously, starting a conversation with the LLM.\n\n*(This example + is complete, it can be run \"as is\", assuming you've [installed the `pydantic_ai` package](install/))*\n\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.\n\nNot very interesting yet, but we can easily add [tools](tools/), [dynamic + instructions](agents/#instructions), and [structured outputs](output/) to build more powerful agents.\n\n## + Tools & Dependency Injection Example\n\nHere is a concise example using Pydantic AI to build a support agent + for a bank:\n\nWith Pydantic AI GatewayDirectly to Provider API\n\n[Learn about Gateway](../gateway) bank\\_support.py\n\n```\nfrom + dataclasses import dataclass\n\nfrom pydantic import BaseModel, Field\nfrom pydantic_ai import Agent, RunContext\n\nfrom + bank_database import DatabaseConn\n\n\n@dataclass\nclass SupportDependencies: # (3)!\n customer_id: int\n + \ db: DatabaseConn # (12)!\n\n\nclass SupportOutput(BaseModel): # (13)!\n support_advice: str = Field(description='Advice + returned to the customer')\n block_card: bool = Field(description=\"Whether to block the customer's card\")\n + \ risk: int = Field(description='Risk level of query', ge=0, le=10)\n\n\nsupport_agent = Agent( # (1)!\n + \ 'gateway/openai:gpt-5', # (2)!\n deps_type=SupportDependencies,\n output_type=SupportOutput, # + (9)!\n instructions=( # (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\n\n@support_agent.instructions # (5)!\nasync + def add_customer_name(ctx: RunContext[SupportDependencies]) -> str:\n customer_name = await ctx.deps.db.customer_name(id=ctx.deps.customer_id)\n + \ return f\"The customer's name is {customer_name!r}\"\n\n\n@support_agent.tool # (6)!\nasync def customer_balance(\n + \ ctx: RunContext[SupportDependencies], include_pending: bool\n) -> float:\n \"\"\"Returns the customer's + current account balance.\"\"\" # (7)!\n return await ctx.deps.db.customer_balance(\n id=ctx.deps.customer_id,\n + \ include_pending=include_pending,\n )\n\n\n... # (11)!\n\n\nasync def main():\n deps = SupportDependencies(customer_id=123, + db=DatabaseConn())\n result = await support_agent.run('What is my balance?', deps=deps) # (8)!\n print(result.output) + \ # (10)!\n \"\"\"\n support_advice='Hello John, your current account balance, including pending transactions, + is $123.45.' block_card=False risk=1\n \"\"\"\n\n result = await support_agent.run('I just lost my card!', + deps=deps)\n print(result.output)\n \"\"\"\n support_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```\n\n1. + 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 type `Agent[SupportDependencies, + SupportOutput]`.\n2. 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.\n3. 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.\n4. Static [instructions](agents/#instructions) can be registered with the [`instructions` keyword + argument](api/agent/#pydantic_ai.agent.Agent.__init__) to the agent.\n5. Dynamic [instructions](agents/#instructions) + can be registered with the [`@agent.instructions`](api/agent/#pydantic_ai.agent.Agent.instructions) decorator, + and can make use of dependency injection. Dependencies are carried via the [`RunContext`](api/tools/#pydantic_ai.tools.RunContext) + argument, which is parameterized with the `deps_type` from above. If the type annotation here is wrong, static + type checkers will catch it.\n6. The [`@agent.tool`](tools/) decorator let you register functions which the + LLM may call while responding to a user. Again, dependencies are carried via [`RunContext`](api/tools/#pydantic_ai.tools.RunContext), + 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.\n7. 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.\n8. [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.\n9. + 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.\n10. The output will be validated with Pydantic to guarantee it is a `SupportOutput`, + since the agent is generic, it'll also be typed as a `SupportOutput` to aid with static type checking.\n11. + 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.\n12. 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.\n13. 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.\n\nbank\\_support.py\n\n```\nfrom + dataclasses import dataclass\n\nfrom pydantic import BaseModel, Field\nfrom pydantic_ai import Agent, RunContext\n\nfrom + bank_database import DatabaseConn\n\n\n@dataclass\nclass SupportDependencies: # (3)!\n customer_id: int\n + \ db: DatabaseConn # (12)!\n\n\nclass SupportOutput(BaseModel): # (13)!\n support_advice: str = Field(description='Advice + returned to the customer')\n block_card: bool = Field(description=\"Whether to block the customer's card\")\n + \ risk: int = Field(description='Risk level of query', ge=0, le=10)\n\n\nsupport_agent = Agent( # (1)!\n + \ 'openai:gpt-5', # (2)!\n deps_type=SupportDependencies,\n output_type=SupportOutput, # (9)!\n + \ instructions=( # (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\n\n@support_agent.instructions # (5)!\nasync def add_customer_name(ctx: + RunContext[SupportDependencies]) -> str:\n customer_name = await ctx.deps.db.customer_name(id=ctx.deps.customer_id)\n + \ return f\"The customer's name is {customer_name!r}\"\n\n\n@support_agent.tool # (6)!\nasync def customer_balance(\n + \ ctx: RunContext[SupportDependencies], include_pending: bool\n) -> float:\n \"\"\"Returns the customer's + current account balance.\"\"\" # (7)!\n return await ctx.deps.db.customer_balance(\n id=ctx.deps.customer_id,\n + \ include_pending=include_pending,\n )\n\n\n... # (11)!\n\n\nasync def main():\n deps = SupportDependencies(customer_id=123, + db=DatabaseConn())\n result = await support_agent.run('What is my balance?', deps=deps) # (8)!\n print(result.output) + \ # (10)!\n \"\"\"\n support_advice='Hello John, your current account balance, including pending transactions, + is $123.45.' block_card=False risk=1\n \"\"\"\n\n result = await support_agent.run('I just lost my card!', + deps=deps)\n print(result.output)\n \"\"\"\n support_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```\n\n1. + 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 type `Agent[SupportDependencies, + SupportOutput]`.\n2. 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.\n3. 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.\n4. Static [instructions](agents/#instructions) can be registered with the [`instructions` keyword + argument](api/agent/#pydantic_ai.agent.Agent.__init__) to the agent.\n5. Dynamic [instructions](agents/#instructions) + can be registered with the [`@agent.instructions`](api/agent/#pydantic_ai.agent.Agent.instructions) decorator, + and can make use of dependency injection. Dependencies are carried via the [`RunContext`](api/tools/#pydantic_ai.tools.RunContext) + argument, which is parameterized with the `deps_type` from above. If the type annotation here is wrong, static + type checkers will catch it.\n6. The [`@agent.tool`](tools/) decorator let you register functions which the + LLM may call while responding to a user. Again, dependencies are carried via [`RunContext`](api/tools/#pydantic_ai.tools.RunContext), + 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.\n7. 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.\n8. [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.\n9. + 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.\n10. The output will be validated with Pydantic to guarantee it is a `SupportOutput`, + since the agent is generic, it'll also be typed as a `SupportOutput` to aid with static type checking.\n11. + 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.\n12. 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.\n13. 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.\n\nComplete + `bank_support.py` example\n\nThe 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/).\n\n## + Instrumentation with Pydantic Logfire\n\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.\nTo understand the flow of the above runs, we can watch the agent in action using Pydantic + Logfire.\n\nTo do this, we need to [set up Logfire](logfire/#using-logfire), and add the following to our + code:\n\nWith Pydantic AI GatewayDirectly to Provider API\n\n[Learn about Gateway](../gateway) bank\\_support\\_with\\_logfire.py\n\n```\n...\nfrom + pydantic_ai import Agent, RunContext\n\nfrom bank_database import DatabaseConn\n\nimport logfire\n\nlogfire.configure() + \ # (1)!\nlogfire.instrument_pydantic_ai() # (2)!\nlogfire.instrument_asyncpg() # (3)!\n\n...\n\nsupport_agent + = Agent(\n 'gateway/openai:gpt-5',\n deps_type=SupportDependencies,\n output_type=SupportOutput,\n + \ system_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```\n\n1. Configure the Logfire SDK, this will fail if project + is not set up.\n2. 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 [`instrument=True` keyword argument](api/agent/#pydantic_ai.agent.Agent.__init__) + to the agent.\n3. In our demo, `DatabaseConn` uses `asyncpg` to connect to a PostgreSQL database, so [`logfire.instrument_asyncpg()`](https://magicstack.github.io/asyncpg/current/) + is used to log the database queries.\n\nbank\\_support\\_with\\_logfire.py\n\n```\n...\nfrom pydantic_ai import + Agent, RunContext\n\nfrom bank_database import DatabaseConn\n\nimport logfire\n\nlogfire.configure() # (1)!\nlogfire.instrument_pydantic_ai() + \ # (2)!\nlogfire.instrument_asyncpg() # (3)!\n\n...\n\nsupport_agent = Agent(\n 'openai:gpt-5',\n deps_type=SupportDependencies,\n + \ output_type=SupportOutput,\n system_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```\n\n1. Configure the + Logfire SDK, this will fail if project is not set up.\n2. 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 [`instrument=True` keyword + argument](api/agent/#pydantic_ai.agent.Agent.__init__) to the agent.\n3. In our demo, `DatabaseConn` uses + `asyncpg` to connect to a PostgreSQL database, so [`logfire.instrument_asyncpg()`](https://magicstack.github.io/asyncpg/current/) + is used to log the database queries.\n\nThat's enough to get the following view of your agent in action:\n\nSee + [Monitoring and Performance](logfire/) to learn more.\n\n## `llms.txt`\n\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.\n\nTwo formats are available:\n\n* [`llms.txt`](https://ai.pydantic.dev/llms.txt): + a file containing a brief description\n of the project, along with links to the different sections of the + documentation. The structure\n of this file is described in details [here](https://llmstxt.org/#format).\n* + [`llms-full.txt`](https://ai.pydantic.dev/llms-full.txt): Similar to the `llms.txt` file,\n but every link + content is included. Note that this file may be too large for some LLMs.\n\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.\n\n## + Next Steps\n\nTo try Pydantic AI for yourself, [install it](install/) and follow the instructions [in the + examples](examples/setup/).\n\nRead the [docs](agents/) to learn more about building applications with Pydantic + AI.\n\nRead the [API Reference](api/agent/) to understand Pydantic AI's interface.\n\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.\n\n\n\n© + Pydantic Services Inc. 2024 to present" media_type: text/plain type: text title: Pydantic AI type: document - retrieved_at: '2025-11-14T23:34:21.151000+00:00' + retrieved_at: '2025-12-08T15:05:59.571000+00:00' type: web_fetch_result url: https://ai.pydantic.dev - tool_use_id: srvtoolu_01So85wNUocinTvFfgKCfQeb + tool_use_id: srvtoolu_01MSZmtbzt6NmQizTETf3GPF 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 + id: msg_015NpiPXidB2wEH4VhabzwgC model: claude-sonnet-4-20250514 role: assistant stop_reason: end_turn @@ -372,8 +362,8 @@ interactions: ephemeral_5m_input_tokens: 0 cache_creation_input_tokens: 0 cache_read_input_tokens: 0 - input_tokens: 7262 - output_tokens: 171 + input_tokens: 11440 + output_tokens: 158 server_tool_use: web_fetch_requests: 1 web_search_requests: 0 @@ -390,7 +380,7 @@ interactions: connection: - keep-alive content-length: - - '21997' + - '34473' content-type: - application/json host: @@ -404,13 +394,12 @@ interactions: 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. + - signature: EooDCkYIChgCKkBe6ddqCwT1CKptPg14EIIaDdq/qCKZucUmb9w4ci/O5pSzheW3Vv33pkeqmFPrcr4l5CWAVM/xaV7NjHNwOPohEgza8xxqBGZ7jds5BzMaDFyd3dP3X5SSgPSg7SIwfyJLZ+yYCfCrpsgV77T7Ay6iBvfLp0ZaSb+ph+2YkSSVAxFDodWxWyv5DiVHRLJnKvEBcESvFzbas8bML22jelhD1nxGiyr6GGBCjavRGaz6LpPExc03FEa9qS0YEvAniVkVxLJprdDcW4Sm2/lLTh6TRiiicyELomVYxI4FrK1R981pA9ZdDgP8iE3YmcvhvvVJpxgUK+6Tr0LJSq5GgnlcPVEcXc+PYzqLNwlHiH+Vm9bPVs/N/JhfYBPKKREWgtXQBvpsxDM8f5Rj6569D7K3oKZ7Dbglc8eWyn1KFYHV4R+sXc4XOHJDPWuEjgW5/aYUfnP5yDlscfTHDgfrZVy7se8rjgtnpwLNstk7GbLUzgIft6p7hTTUIGB7icopfZtoahgB + thinking: The user wants me to fetch the content from https://ai.pydantic.dev and return only the first sentence. + I need to use the web_fetch tool to get the content from this URL, then identify the first sentence and return + only that. type: thinking - - id: srvtoolu_01So85wNUocinTvFfgKCfQeb + - id: srvtoolu_01MSZmtbzt6NmQizTETf3GPF input: url: https://ai.pydantic.dev name: web_fetch @@ -419,295 +408,290 @@ interactions: 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. + data: "Pydantic AI\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n \n\n\n\n\n\n[Skip to content](#pydantic-ai)\n\n**[Pydantic + AI Gateway](/gateway) is now available! \U0001F680\nEnterprise-ready AI model routing: One key for all your + models with real-time monitoring and budget control that works.**\n\n[![logo](img/logo-white.svg)](. \"Pydantic + AI\")\n\n\n\n\nPydantic AI\n\nPydantic AI\n\n\n\n\n\n\n\nType to start searching\n\n[pydantic/pydantic-ai](https://github.com/pydantic/pydantic-ai + \"Go to repository\")\n\n[![logo](img/logo-white.svg)](. \"Pydantic AI\")\nPydantic AI\n\n[pydantic/pydantic-ai](https://github.com/pydantic/pydantic-ai + \"Go to repository\")\n\n* Pydantic AI\n\n [Pydantic AI](.)\n\n\n\n Table of contents\n + [Why use Pydantic + AI](#why-use-pydantic-ai)\n + [Hello World Example](#hello-world-example)\n + [Tools & Dependency Injection + Example](#tools-dependency-injection-example)\n + [Instrumentation with Pydantic Logfire](#instrumentation-with-pydantic-logfire)\n + \ + [llms.txt](#llmstxt)\n + [Next Steps](#next-steps)\n* [Installation](install/)\n* [Getting Help](help/)\n* + [Troubleshooting](troubleshooting/)\n* [Pydantic AI Gateway](gateway/)\n* Documentation\n\n\n\n\n Documentation\n + \ + Core Concepts\n\n\n\n\n Core Concepts\n - [Agents](agents/)\n - [Dependencies](dependencies/)\n + \ - [Function Tools](tools/)\n - [Output](output/)\n - [Messages and chat history](message-history/)\n + \ - [Direct Model Requests](direct/)\n + Models & Providers\n\n\n\n\n Models & Providers\n - [Overview](models/overview/)\n + \ - [OpenAI](models/openai/)\n - [Anthropic](models/anthropic/)\n - [Google](models/google/)\n - + [Bedrock](models/bedrock/)\n - [Cohere](models/cohere/)\n - [Groq](models/groq/)\n - [Hugging Face](models/huggingface/)\n + \ - [Mistral](models/mistral/)\n - [OpenRouter](models/openrouter/)\n - [Outlines](models/outlines/)\n + \ + Tools & Toolsets\n\n\n\n\n Tools & Toolsets\n - [Function Tools](tools/)\n - [Advanced Tool + Features](tools-advanced/)\n - [Toolsets](toolsets/)\n - [Deferred Tools](deferred-tools/)\n - + [Built-in Tools](builtin-tools/)\n - [Common Tools](common-tools/)\n - [Third-Party Tools](third-party-tools/)\n + \ + Advanced Features\n\n\n\n\n Advanced Features\n - [Image, Audio, Video & Document Input](input/)\n + \ - [Thinking](thinking/)\n - [HTTP Request Retries](retries/)\n + MCP\n\n\n\n\n MCP\n - [Overview](mcp/overview/)\n + \ - [Client](mcp/client/)\n - [FastMCP Client](mcp/fastmcp-client/)\n - [Server](mcp/server/)\n + \ + [Multi-Agent Patterns](multi-agent-applications/)\n + [Testing](testing/)\n* Pydantic Evals\n\n\n\n\n + \ Pydantic Evals\n + [Overview](evals/)\n + Getting Started\n\n\n\n\n Getting Started\n - [Quick + Start](evals/quick-start/)\n - [Core Concepts](evals/core-concepts/)\n + Evaluators\n\n\n\n\n Evaluators\n + \ - [Overview](evals/evaluators/overview/)\n - [Built-in Evaluators](evals/evaluators/built-in/)\n + \ - [LLM Judge](evals/evaluators/llm-judge/)\n - [Custom Evaluators](evals/evaluators/custom/)\n - + [Span-Based](evals/evaluators/span-based/)\n + How-To Guides\n\n\n\n\n How-To Guides\n - [Logfire + Integration](evals/how-to/logfire-integration/)\n - [Dataset Management](evals/how-to/dataset-management/)\n + \ - [Dataset Serialization](evals/how-to/dataset-serialization/)\n - [Concurrency & Performance](evals/how-to/concurrency/)\n + \ - [Retry Strategies](evals/how-to/retry-strategies/)\n - [Metrics & Attributes](evals/how-to/metrics-attributes/)\n + \ + Examples\n\n\n\n\n Examples\n - [Simple Validation](evals/examples/simple-validation/)\n* Pydantic + Graph\n\n\n\n\n Pydantic Graph\n + [Overview](graph/)\n + [Beta API](graph/beta/)\n\n Beta API\n - + [Steps](graph/beta/steps/)\n - [Joins & Reducers](graph/beta/joins/)\n - [Decisions](graph/beta/decisions/)\n + \ - [Parallel Execution](graph/beta/parallel/)\n* Integrations\n\n\n\n\n Integrations\n + [Debugging + & Monitoring with Pydantic Logfire](logfire/)\n + Durable Execution\n\n\n\n\n Durable Execution\n - + [Overview](durable_execution/overview/)\n - [Temporal](durable_execution/temporal/)\n - [DBOS](durable_execution/dbos/)\n + \ - [Prefect](durable_execution/prefect/)\n + UI Event Streams\n\n\n\n\n UI Event Streams\n - [Overview](ui/overview/)\n + \ - [AG-UI](ui/ag-ui/)\n - [Vercel AI](ui/vercel-ai/)\n + [Agent2Agent (A2A)](a2a/)\n* Related Packages\n\n\n\n\n + \ Related Packages\n + [Clai](cli/)\n* Examples\n\n\n\n\n Examples\n + [Setup](examples/setup/)\n + + Getting Started\n\n\n\n\n Getting Started\n - [Pydantic Model](examples/pydantic-model/)\n - [Weather + agent](examples/weather-agent/)\n + Conversational Agents\n\n\n\n\n Conversational Agents\n - [Chat + App with FastAPI](examples/chat-app/)\n - [Bank support](examples/bank-support/)\n + Data & Analytics\n\n\n\n\n + \ Data & Analytics\n - [SQL Generation](examples/sql-gen/)\n - [Data Analyst](examples/data-analyst/)\n + \ - [RAG](examples/rag/)\n + Streaming\n\n\n\n\n Streaming\n - [Stream markdown](examples/stream-markdown/)\n + \ - [Stream whales](examples/stream-whales/)\n + Complex Workflows\n\n\n\n\n Complex Workflows\n - + [Flight booking](examples/flight-booking/)\n - [Question Graph](examples/question-graph/)\n + Business + Applications\n\n\n\n\n Business Applications\n - [Slack Lead Qualifier with Modal](examples/slack-lead-qualifier/)\n + \ + UI Examples\n\n\n\n\n UI Examples\n - [Agent User Interaction (AG-UI)](examples/ag-ui/)\n* API + Reference\n\n\n\n\n API Reference\n + pydantic\\_ai\n\n\n\n\n pydantic\\_ai\n - [pydantic\\_ai.agent](api/agent/)\n + \ - [pydantic\\_ai.tools](api/tools/)\n - [pydantic\\_ai.toolsets](api/toolsets/)\n - [pydantic\\_ai.builtin\\_tools](api/builtin_tools/)\n + \ - [pydantic\\_ai.common\\_tools](api/common_tools/)\n - [pydantic\\_ai.durable\\_exec](api/durable_exec/)\n + \ - [pydantic\\_ai.output](api/output/)\n - [pydantic\\_ai.result](api/result/)\n - [pydantic\\_ai.messages](api/messages/)\n + \ - [pydantic\\_ai.exceptions](api/exceptions/)\n - [pydantic\\_ai.settings](api/settings/)\n - + [pydantic\\_ai.usage](api/usage/)\n - [pydantic\\_ai.mcp](api/mcp/)\n - [pydantic\\_ai.format\\_prompt](api/format_prompt/)\n + \ - [pydantic\\_ai.direct](api/direct/)\n - [pydantic\\_ai.ext](api/ext/)\n - [pydantic\\_ai.models.anthropic](api/models/anthropic/)\n + \ - [pydantic\\_ai.models](api/models/base/)\n - [pydantic\\_ai.models.bedrock](api/models/bedrock/)\n + \ - [pydantic\\_ai.models.cohere](api/models/cohere/)\n - [pydantic\\_ai.models.fallback](api/models/fallback/)\n + \ - [pydantic\\_ai.models.function](api/models/function/)\n - [pydantic\\_ai.models.google](api/models/google/)\n + \ - [pydantic\\_ai.models.groq](api/models/groq/)\n - [pydantic\\_ai.models.huggingface](api/models/huggingface/)\n + \ - [pydantic\\_ai.models.instrumented](api/models/instrumented/)\n - [pydantic\\_ai.models.mcp\\_sampling](api/models/mcp-sampling/)\n + \ - [pydantic\\_ai.models.mistral](api/models/mistral/)\n - [pydantic\\_ai.models.openai](api/models/openai/)\n + \ - [pydantic\\_ai.models.openrouter](api/models/openrouter/)\n - [pydantic\\_ai.models.outlines](api/models/outlines/)\n + \ - [pydantic\\_ai.models.test](api/models/test/)\n - [pydantic\\_ai.models.wrapper](api/models/wrapper/)\n + \ - [pydantic\\_ai.profiles](api/profiles/)\n - [pydantic\\_ai.providers](api/providers/)\n - [pydantic\\_ai.retries](api/retries/)\n + \ - [pydantic\\_ai.run](api/run/)\n - [pydantic\\_ai.ag\\_ui](api/ag_ui/)\n - [pydantic\\_ai.ui](api/ui/base/)\n + \ - [pydantic\\_ai.ui.ag\\_ui](api/ui/ag_ui/)\n - [pydantic\\_ai.ui.vercel\\_ai](api/ui/vercel_ai/)\n + \ + pydantic\\_evals\n\n\n\n\n pydantic\\_evals\n - [pydantic\\_evals.dataset](api/pydantic_evals/dataset/)\n + \ - [pydantic\\_evals.evaluators](api/pydantic_evals/evaluators/)\n - [pydantic\\_evals.reporting](api/pydantic_evals/reporting/)\n + \ - [pydantic\\_evals.otel](api/pydantic_evals/otel/)\n - [pydantic\\_evals.generation](api/pydantic_evals/generation/)\n + \ + pydantic\\_graph\n\n\n\n\n pydantic\\_graph\n - [pydantic\\_graph](api/pydantic_graph/graph/)\n + \ - [pydantic\\_graph.nodes](api/pydantic_graph/nodes/)\n - [pydantic\\_graph.persistence](api/pydantic_graph/persistence/)\n + \ - [pydantic\\_graph.mermaid](api/pydantic_graph/mermaid/)\n - [pydantic\\_graph.exceptions](api/pydantic_graph/exceptions/)\n + \ - Beta API\n\n\n\n\n Beta API\n * [pydantic\\_graph.beta](api/pydantic_graph/beta/)\n * + [pydantic\\_graph.beta.graph](api/pydantic_graph/beta_graph/)\n * [pydantic\\_graph.beta.graph\\_builder](api/pydantic_graph/beta_graph_builder/)\n + \ * [pydantic\\_graph.beta.step](api/pydantic_graph/beta_step/)\n * [pydantic\\_graph.beta.join](api/pydantic_graph/beta_join/)\n + \ * [pydantic\\_graph.beta.decision](api/pydantic_graph/beta_decision/)\n * [pydantic\\_graph.beta.node](api/pydantic_graph/beta_node/)\n + \ + fasta2a\n\n\n\n\n fasta2a\n - [fasta2a](api/fasta2a/)\n* Project\n\n\n\n\n Project\n + [Contributing](contributing/)\n + \ + [Upgrade Guide](changelog/)\n + [Version policy](version-policy/)\n\nTable of contents\n\n* [Why use + Pydantic AI](#why-use-pydantic-ai)\n* [Hello World Example](#hello-world-example)\n* [Tools & Dependency + Injection Example](#tools-dependency-injection-example)\n* [Instrumentation with Pydantic Logfire](#instrumentation-with-pydantic-logfire)\n* + [llms.txt](#llmstxt)\n* [Next Steps](#next-steps)\n\n# Pydantic AI\n\n![Pydantic AI](./img/pydantic-ai-dark.svg#only-dark)\n\n![Pydantic + AI](./img/pydantic-ai-light.svg#only-light)\n\n*GenAI Agent Framework, the Pydantic way*\n\n[![CI](https://github.com/pydantic/pydantic-ai/actions/workflows/ci.yml/badge.svg?event=push)](https://github.com/pydantic/pydantic-ai/actions/workflows/ci.yml?query=branch%3Amain)\n[![Coverage](https://coverage-badge.samuelcolvin.workers.dev/pydantic/pydantic-ai.svg)](https://coverage-badge.samuelcolvin.workers.dev/redirect/pydantic/pydantic-ai)\n[![PyPI](https://img.shields.io/pypi/v/pydantic-ai.svg)](https://pypi.python.org/pypi/pydantic-ai)\n[![versions](https://img.shields.io/pypi/pyversions/pydantic-ai.svg)](https://github.com/pydantic/pydantic-ai)\n[![license](https://img.shields.io/github/license/pydantic/pydantic-ai.svg)](https://github.com/pydantic/pydantic-ai/blob/main/LICENSE)\n[![Join + Slack](https://img.shields.io/badge/Slack-Join%20Slack-4A154B?logo=slack)](https://logfire.pydantic.dev/docs/join-slack/)\n\nPydantic + AI is a Python agent framework designed to help you\nquickly, confidently, and painlessly build production + grade applications and workflows with Generative AI.\n\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.\n\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.\n\nWe built Pydantic AI with one simple aim: to + bring that FastAPI feeling to GenAI app and agent development.\n\n## Why use Pydantic AI\n\n1. **Built 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?* ![\U0001F603](https://cdn.jsdelivr.net/gh/jdecked/twemoji@15.1.0/assets/svg/1f603.svg + \":smiley:\")\n2. **Model-agnostic**:\n 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).\n3. **Seamless Observability**:\n + \ 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).\n4. **Fully Type-safe**:\n 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.\n5. **Powerful Evals**:\n 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.\n6. + **MCP, A2A, and UI**:\n 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.\n7. + **Human-in-the-Loop Tool Approval**:\n 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.\n8. + **Durable Execution**:\n 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.\n9. **Streamed Outputs**:\n + \ Provides the ability to [stream](output/#streamed-results) structured output continuously, with immediate + validation, ensuring real time access to generated data.\n10. **Graph Support**:\n 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.\n\nRealistically though, no list is going to be as convincing as [giving + it a try](#next-steps) and seeing how it makes you feel!\n\n**Sign up for our newsletter, *The Pydantic + Stack*, with updates & tutorials on Pydantic AI, Logfire, and Pydantic:**\n\nSubscribe\n\n## Hello World + Example\n\nHere's a minimal example of Pydantic AI:\n\nWith Pydantic AI GatewayDirectly to Provider API\n\n[Learn + about Gateway](../gateway) hello\\_world.py\n\n```\nfrom pydantic_ai import Agent\n\nagent = Agent( # (1)!\n + \ 'gateway/anthropic:claude-sonnet-4-0',\n instructions='Be concise, reply with one sentence.', # + (2)!\n)\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```\n\n1. + 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.\n2. Register static [instructions](agents/#instructions) using + a keyword argument to the agent.\n3. [Run the agent](agents/#running-agents) synchronously, starting a conversation + with the LLM.\n\nhello\\_world.py\n\n```\nfrom pydantic_ai import Agent\n\nagent = Agent( # (1)!\n 'anthropic:claude-sonnet-4-0',\n + \ instructions='Be concise, reply with one sentence.', # (2)!\n)\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```\n\n1. 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.\n2. Register static [instructions](agents/#instructions) using a keyword argument to the agent.\n3. + [Run the agent](agents/#running-agents) synchronously, starting a conversation with the LLM.\n\n*(This example + is complete, it can be run \"as is\", assuming you've [installed the `pydantic_ai` package](install/))*\n\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.\n\nNot very interesting yet, but we can easily add [tools](tools/), + [dynamic instructions](agents/#instructions), and [structured outputs](output/) to build more powerful agents.\n\n## + Tools & Dependency Injection Example\n\nHere is a concise example using Pydantic AI to build a support agent + for a bank:\n\nWith Pydantic AI GatewayDirectly to Provider API\n\n[Learn about Gateway](../gateway) bank\\_support.py\n\n```\nfrom + dataclasses import dataclass\n\nfrom pydantic import BaseModel, Field\nfrom pydantic_ai import Agent, RunContext\n\nfrom + bank_database import DatabaseConn\n\n\n@dataclass\nclass SupportDependencies: # (3)!\n customer_id: + int\n db: DatabaseConn # (12)!\n\n\nclass SupportOutput(BaseModel): # (13)!\n support_advice: str + = Field(description='Advice returned to the customer')\n block_card: bool = Field(description=\"Whether + to block the customer's card\")\n risk: int = Field(description='Risk level of query', ge=0, le=10)\n\n\nsupport_agent + = Agent( # (1)!\n 'gateway/openai:gpt-5', # (2)!\n deps_type=SupportDependencies,\n output_type=SupportOutput, + \ # (9)!\n instructions=( # (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\n\n@support_agent.instructions # (5)!\nasync + def add_customer_name(ctx: RunContext[SupportDependencies]) -> str:\n customer_name = await ctx.deps.db.customer_name(id=ctx.deps.customer_id)\n + \ return f\"The customer's name is {customer_name!r}\"\n\n\n@support_agent.tool # (6)!\nasync def customer_balance(\n + \ ctx: RunContext[SupportDependencies], include_pending: bool\n) -> float:\n \"\"\"Returns the customer's + current account balance.\"\"\" # (7)!\n return await ctx.deps.db.customer_balance(\n id=ctx.deps.customer_id,\n + \ include_pending=include_pending,\n )\n\n\n... # (11)!\n\n\nasync def main():\n deps = SupportDependencies(customer_id=123, + db=DatabaseConn())\n result = await support_agent.run('What is my balance?', deps=deps) # (8)!\n print(result.output) + \ # (10)!\n \"\"\"\n support_advice='Hello John, your current account balance, including pending transactions, + is $123.45.' block_card=False risk=1\n \"\"\"\n\n result = await support_agent.run('I just lost my + card!', deps=deps)\n print(result.output)\n \"\"\"\n support_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```\n\n1. + 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 type `Agent[SupportDependencies, + SupportOutput]`.\n2. 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.\n3. 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.\n4. Static [instructions](agents/#instructions) can be registered with the [`instructions` keyword + argument](api/agent/#pydantic_ai.agent.Agent.__init__) to the agent.\n5. Dynamic [instructions](agents/#instructions) + can be registered with the [`@agent.instructions`](api/agent/#pydantic_ai.agent.Agent.instructions) decorator, + and can make use of dependency injection. Dependencies are carried via the [`RunContext`](api/tools/#pydantic_ai.tools.RunContext) + argument, which is parameterized with the `deps_type` from above. If the type annotation here is wrong, + static type checkers will catch it.\n6. The [`@agent.tool`](tools/) decorator let you register functions + which the LLM may call while responding to a user. Again, dependencies are carried via [`RunContext`](api/tools/#pydantic_ai.tools.RunContext), + 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.\n7. 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.\n8. [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.\n9. + 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.\n10. The output will be validated with Pydantic to guarantee it is a + `SupportOutput`, since the agent is generic, it'll also be typed as a `SupportOutput` to aid with static + type checking.\n11. 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.\n12. 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.\n13. 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.\n\nbank\\_support.py\n\n```\nfrom dataclasses import dataclass\n\nfrom + pydantic import BaseModel, Field\nfrom pydantic_ai import Agent, RunContext\n\nfrom bank_database import + DatabaseConn\n\n\n@dataclass\nclass SupportDependencies: # (3)!\n customer_id: int\n db: DatabaseConn + \ # (12)!\n\n\nclass SupportOutput(BaseModel): # (13)!\n support_advice: str = Field(description='Advice + returned to the customer')\n block_card: bool = Field(description=\"Whether to block the customer's card\")\n + \ risk: int = Field(description='Risk level of query', ge=0, le=10)\n\n\nsupport_agent = Agent( # (1)!\n + \ 'openai:gpt-5', # (2)!\n deps_type=SupportDependencies,\n output_type=SupportOutput, # (9)!\n + \ instructions=( # (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\n\n@support_agent.instructions # (5)!\nasync + def add_customer_name(ctx: RunContext[SupportDependencies]) -> str:\n customer_name = await ctx.deps.db.customer_name(id=ctx.deps.customer_id)\n + \ return f\"The customer's name is {customer_name!r}\"\n\n\n@support_agent.tool # (6)!\nasync def customer_balance(\n + \ ctx: RunContext[SupportDependencies], include_pending: bool\n) -> float:\n \"\"\"Returns the customer's + current account balance.\"\"\" # (7)!\n return await ctx.deps.db.customer_balance(\n id=ctx.deps.customer_id,\n + \ include_pending=include_pending,\n )\n\n\n... # (11)!\n\n\nasync def main():\n deps = SupportDependencies(customer_id=123, + db=DatabaseConn())\n result = await support_agent.run('What is my balance?', deps=deps) # (8)!\n print(result.output) + \ # (10)!\n \"\"\"\n support_advice='Hello John, your current account balance, including pending transactions, + is $123.45.' block_card=False risk=1\n \"\"\"\n\n result = await support_agent.run('I just lost my + card!', deps=deps)\n print(result.output)\n \"\"\"\n support_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```\n\n1. + 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 type `Agent[SupportDependencies, + SupportOutput]`.\n2. 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.\n3. 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.\n4. Static [instructions](agents/#instructions) can be registered with the [`instructions` keyword + argument](api/agent/#pydantic_ai.agent.Agent.__init__) to the agent.\n5. Dynamic [instructions](agents/#instructions) + can be registered with the [`@agent.instructions`](api/agent/#pydantic_ai.agent.Agent.instructions) decorator, + and can make use of dependency injection. Dependencies are carried via the [`RunContext`](api/tools/#pydantic_ai.tools.RunContext) + argument, which is parameterized with the `deps_type` from above. If the type annotation here is wrong, + static type checkers will catch it.\n6. The [`@agent.tool`](tools/) decorator let you register functions + which the LLM may call while responding to a user. Again, dependencies are carried via [`RunContext`](api/tools/#pydantic_ai.tools.RunContext), + 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.\n7. 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.\n8. [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.\n9. + 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.\n10. The output will be validated with Pydantic to guarantee it is a + `SupportOutput`, since the agent is generic, it'll also be typed as a `SupportOutput` to aid with static + type checking.\n11. 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.\n12. 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.\n13. 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.\n\nComplete `bank_support.py` example\n\nThe 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/).\n\n## Instrumentation with Pydantic Logfire\n\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.\nTo understand the flow of the above runs, + we can watch the agent in action using Pydantic Logfire.\n\nTo do this, we need to [set up Logfire](logfire/#using-logfire), + and add the following to our code:\n\nWith Pydantic AI GatewayDirectly to Provider API\n\n[Learn about Gateway](../gateway) + bank\\_support\\_with\\_logfire.py\n\n```\n...\nfrom pydantic_ai import Agent, RunContext\n\nfrom bank_database + import DatabaseConn\n\nimport logfire\n\nlogfire.configure() # (1)!\nlogfire.instrument_pydantic_ai() # + (2)!\nlogfire.instrument_asyncpg() # (3)!\n\n...\n\nsupport_agent = Agent(\n 'gateway/openai:gpt-5',\n + \ deps_type=SupportDependencies,\n output_type=SupportOutput,\n system_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```\n\n1. Configure the Logfire SDK, this will fail if project is not set up.\n2. 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 [`instrument=True` keyword argument](api/agent/#pydantic_ai.agent.Agent.__init__) to the agent.\n3. + In our demo, `DatabaseConn` uses `asyncpg` to connect to a PostgreSQL database, so [`logfire.instrument_asyncpg()`](https://magicstack.github.io/asyncpg/current/) + is used to log the database queries.\n\nbank\\_support\\_with\\_logfire.py\n\n```\n...\nfrom pydantic_ai + import Agent, RunContext\n\nfrom bank_database import DatabaseConn\n\nimport logfire\n\nlogfire.configure() + \ # (1)!\nlogfire.instrument_pydantic_ai() # (2)!\nlogfire.instrument_asyncpg() # (3)!\n\n...\n\nsupport_agent + = Agent(\n 'openai:gpt-5',\n deps_type=SupportDependencies,\n output_type=SupportOutput,\n system_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```\n\n1. Configure the Logfire SDK, this will fail if project is not set up.\n2. + 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 [`instrument=True` keyword argument](api/agent/#pydantic_ai.agent.Agent.__init__) + to the agent.\n3. In our demo, `DatabaseConn` uses `asyncpg` to connect to a PostgreSQL database, so [`logfire.instrument_asyncpg()`](https://magicstack.github.io/asyncpg/current/) + is used to log the database queries.\n\nThat's enough to get the following view of your agent in action:\n\nSee + [Monitoring and Performance](logfire/) to learn more.\n\n## `llms.txt`\n\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.\n\nTwo formats are available:\n\n* [`llms.txt`](https://ai.pydantic.dev/llms.txt): + a file containing a brief description\n of the project, along with links to the different sections of the + documentation. The structure\n of this file is described in details [here](https://llmstxt.org/#format).\n* + [`llms-full.txt`](https://ai.pydantic.dev/llms-full.txt): Similar to the `llms.txt` file,\n but every link + content is included. Note that this file may be too large for some LLMs.\n\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.\n\n## Next Steps\n\nTo try Pydantic AI for yourself, [install it](install/) and follow the instructions + [in the examples](examples/setup/).\n\nRead the [docs](agents/) to learn more about building applications + with Pydantic AI.\n\nRead the [API Reference](api/agent/) to understand Pydantic AI's interface.\n\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.\n\n\n\n© Pydantic Services Inc. 2024 to present" media_type: text/plain type: text title: Pydantic AI type: document - retrieved_at: '2025-11-14T23:34:21.151000+00:00' + retrieved_at: '2025-12-08T15:05:59.571000+00:00' type: web_fetch_result url: https://ai.pydantic.dev - tool_use_id: srvtoolu_01So85wNUocinTvFfgKCfQeb + tool_use_id: srvtoolu_01MSZmtbzt6NmQizTETf3GPF 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. @@ -738,37 +722,49 @@ interactions: connection: - keep-alive content-length: - - '3150' + - '4730' content-type: - application/json retry-after: - - '36' + - '47' 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 + - signature: EoMLCkYIChgCKkAJqNs+Sem70XQqD4BTpMDThWZMMjR2WEgJKIlYvj4B8noeoZolpVe9rsKZqYy37KFQfVlgKVvznIhNoGYABCGdEgzfs4fFZUkmJ8pta9saDHNxHH70HOLepd+lICIwfnIjiGn76xEZI5AhKLCBCXvVWIaOJvYwmyZ2XTRot7v9VHCdnqpM3dS5NeU/BL6wKuoJpOdesmsKzzPUz2OpeaaCrD+0CDnHdzIHv1tM6kXMJvqrj4boQ+OE/s5FcFWwGArl0/SW3NwT11RoqFQQVI2ekTF8EqFIoixUBnRhwGEk+LauI+f429lKWoEvKP7bNAWrsu3j+jbBRNevFAk2BmfoDP9bfZtLAh1y/9topi6x5xvFNR4nNk/pXnkQLMjwAwjq63e4YZzxg3mMZ4e1Rzoyb18c6/TQgvZMYog87HCyWf+yJS9zVQxbl33szJcujnG6EHYRHw33Kb3/z73Sedv+FJgGf4oC1U9CPB1jErLkbhBb48yxpCdU1AZH+1jzgE4Gt1wY2yax7zG53m7IYvuELSM4H2mR9yF/V3WdmhTad8cOb1MtNek7JTsGPxxPd/FqKVLCl8vGXLz8W6GUzbSrtXtk0gnoiWoWbPzAj92K285k7fN7SfgsQPPhJseoDQg7CRL7tdU6xKYph0AsWsjPcv0x0xNwEW5L5uSMFgEZUmponS/pKOJYxuO9HcanU2GRyN91YEa3KrQDye8Pjei01iL/tVUYmQ4xHxCruNFbY7fVSqlYmc2r68sixCjSi8it3/MrR9ZtMZ8ujU9S50raPoP9Vy6fkfodP0BEEKADC/tSuZuDIr4TpZrtlLOIhDOzmzIV6+5biyQhDT28xypeASOv+M4KMjMaGIenZWMr0p2ER/xtK0Q+z9pNGBSDsP1E5jGjMxs25lObHkw8WHpssPf6hzEfxRYMAUriB+NHAuWfvro3pc+T8/yEWw31iW8vLwgZ0FdO3rabvCl0rIo6Wcu1pozHN63TSOP2p8vLtzWUEvOjLb0mR5BJgqZ8yqa3jyt1DrNyivXUuXlw0xqOcZwwWB5y1a2QDKAY5uzI37oEntLYCJ0fcyy0/stQiy5kbQbzDgIyQje9+NpT7CcoH6MURuR9QnUJ33K7/D+LzI7YUSJo8zT5szNhYov9m54+4klKTDnjuYGtFlT6tivkEdkybLPXSlKtrmvHZNCoJkZ3qlK5POwVJYTbLqm73INRttG+mPD3fH7GLLgF3eHvJOq0VzvXYT0j31tSe+zrPskwEqbl/ghPUR579KbQeWMmi9AeyD1F/MBUFf7nBqu27yCcH0oje+LjEN4sE1A9RPCXXdPC1zFPVgwfoSR/IR7xCRvL4Dp70QxBRujzZW008wRg2AXR+l5Dfz+OSfh4hCXg91GnVZmq8iNSL77yfwwK9o3EK48REbRV+ezJ3DQFeUrkDSw8tXCnpCtqIL/L74s7e0OYbgxpITrKEkOt4eMhTbWJY6JgwhUdViVKhdlf1OU6WRyOptZl1kRtKm7khQZ7ThSEaIDdRsndHR8cRBhHV+h31wKw6bgMvOQmqvAdljPXGPqkDLKDDrGkTMHn2fyHxRmCm24edSpNkBtIzyzMwlG9k33UP4ITly7vkcQCXxP+TD9qXdD33i/WzYYdiu5Vhfdxhxo9kUPDcUzp03eqBm2BetsXdva2+Nk0wy6ozUF+1wlqdMYBhf/JkorunuSB+dLW1lnp+c7GVRWKOWvljncJ0I6uWFD8B/kcH4eIQx49Cb0bZf2rDgVXqAkwKaBptupW6+AdDZfCB5Cs9DCXUVgFHSGljeCn2Rbelig68ncUwxio53luA3ldtfE8QV4uGSOf11OW0mnKOS6GHDro0SggtvsuL3eoVhgB 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: + The user is asking about what framework is mentioned on the page I just fetched from https://ai.pydantic.dev. Let me look through the content to identify the frameworks mentioned. + + Looking at the content, I can see several frameworks mentioned: + + 1. **Pydantic AI** - This is the main framework the page is about. It's described as "a Python agent framework designed to help you quickly, confidently, and painlessly build production grade applications and workflows with Generative AI." + + 2. **FastAPI** - The page mentions "FastAPI revolutionized web development by offering an innovative and ergonomic design, built on the foundation of Pydantic Validation and modern Python features like type hints." It also says "We built Pydantic AI with one simple aim: to bring that FastAPI feeling to GenAI app and agent development." - 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 + 3. **Pydantic Validation** - Mentioned as the foundation that FastAPI is built on. - 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." + 4. **Other frameworks mentioned in passing**: LangChain, LlamaIndex, AutoGPT, Transformers, CrewAI, Instructor (these are mentioned as examples of libraries that use Pydantic Validation) + + The primary framework the page is about is **Pydantic AI**, but it also prominently mentions **FastAPI** as an inspiration and comparison point. 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." + - text: |- + Based on the page I fetched, several frameworks are mentioned: + + **Primary framework:** + - **Pydantic AI** - The main subject of the page, described as "a Python agent framework designed to help you quickly, confidently, and painlessly build production grade applications and workflows with Generative AI." + + **Key comparison framework:** + - **FastAPI** - Prominently mentioned as the inspiration, with the page stating: "FastAPI revolutionized web development by offering an innovative and ergonomic design, built on the foundation of Pydantic Validation and modern Python features like type hints" and "We built Pydantic AI with one simple aim: to bring that FastAPI feeling to GenAI app and agent development." + + **Other frameworks mentioned:** + - **Pydantic Validation** (the foundation both are built on) + - **LangChain, LlamaIndex, AutoGPT, Transformers, CrewAI, Instructor** (mentioned as examples of libraries that use Pydantic Validation) + + The page positions Pydantic AI as bringing the same innovative and ergonomic design philosophy that made FastAPI successful to the GenAI and agent development space. type: text - id: msg_01SJJ9cZeR6yBpdy3Lf1Lx5i + id: msg_01Ea11Z69qskEERXpgbimoGg model: claude-sonnet-4-20250514 role: assistant stop_reason: end_turn @@ -780,8 +776,8 @@ interactions: ephemeral_5m_input_tokens: 0 cache_creation_input_tokens: 0 cache_read_input_tokens: 0 - input_tokens: 6346 - output_tokens: 354 + input_tokens: 10537 + output_tokens: 568 service_tier: standard status: code: 200 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 index d03bb3a332..9dbd8d8ed3 100644 --- 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 @@ -8,7 +8,7 @@ interactions: connection: - keep-alive content-length: - - '361' + - '466' content-type: - application/json host: @@ -29,168 +29,327 @@ interactions: tool_choice: type: auto tools: - - name: web_fetch + - 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: 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" } - + string: "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-20250514\",\"id\":\"msg_01JxeuU5zQRPduCDXsDQhMry\",\"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\":8,\"service_tier\":\"standard\"}} + \ }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"thinking\",\"thinking\":\"\",\"signature\":\"\"} + \ }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"The + user is asking for the first sentence\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent: content_block_delta\ndata: + {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" on a specific\"} + \ }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" + web page. I need to use the\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" + web_fetch tool to get\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" + the content from that URL an\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"d + then identify\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" + the first sentence.\\n\\nThe\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" + URL provide\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"d + is https\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"://ai.pydantic.dev\"} + }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" + -\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" + this appears to be a vali\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"d + URL with\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" + the\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" + https\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" + schema\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" + include\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"d.\"} + \ }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"\"} + \ }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"signature_delta\",\"signature\":\"EsADCkYIChgCKkA58AqPyRf+zhQwXZM7L/AlQu/MXqlBYT9D+Iin18ScuuGOJF9C0G2tTRtuap+cryX9hT5kc/qcUYlnIb9Vm8BfEgzmZTiT2aAYFcrlsqwaDPzuOGNFvVPoClWurCIwmtlSAwBmrVAOblmM9pdEC3LDbBdJ43qZlP3EHrxuV+Z7/XxW9raDXU4Kz0Ug+GiEKqcC+uDpVdQEtTxJYYxouugfzpunt5grRDUECpgdkXY0u8Hy8XhfwaIkEOmkY8RckI9fqatCczffZ4fWztKzw1Nygxqmox7NgZhw+NBuliEjr3Mm1L+bKbDuOxsjMlOrl2fRMNIupBiJWVP+qiStetD1mF+8+qEjb01vHaWUjScsNhmxus4+zJGVu9CyV42rnw9pb4v3w4vvuaBxw3GpanyXjbRaOP/L5v16kxIM/tZzJb8TQw8R8rFnIWPLSKZ2mT/r5js1A+f3DKb41dEe8T21JyZCR5PkBoWP+xmvxzbp9/zCl3CeVfGXG18NtigQwo2yvi0N4TC4sGHmcAMKafbvt+TwJODEmBBYBW/zYaqUyYjxhz04g29Ji2WKqUQf+8eg+SSbfc8LcBgB\"} + \ }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 }\n\nevent: + content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"server_tool_use\",\"id\":\"srvtoolu_011CmugtvU55veq6jzEtGtnt\",\"name\":\"web_fetch\",\"input\":{}} + \ }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"} + \ }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"ur\"} + }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"l\\\": + \\\"htt\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"ps://ai\"} + \ }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\".pydantic\"} + \ }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\".dev\\\"}\"} + \ }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":1 }\n\nevent: + content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":2,\"content_block\":{\"type\":\"web_fetch_tool_result\",\"tool_use_id\":\"srvtoolu_011CmugtvU55veq6jzEtGtnt\",\"content\":{\"type\":\"web_fetch_result\",\"url\":\"https://ai.pydantic.dev\",\"retrieved_at\":\"2025-12-08T15:05:59.571000+00:00\",\"content\":{\"type\":\"document\",\"source\":{\"type\":\"text\",\"media_type\":\"text/plain\",\"data\":\"Pydantic + AI\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n \\n\\n\\n\\n\\n\\n[Skip to content](#pydantic-ai)\\n\\n**[Pydantic + AI Gateway](/gateway) is now available! \U0001F680\\nEnterprise-ready AI model routing: One key for all your models + with real-time monitoring and budget control that works.**\\n\\n[![logo](img/logo-white.svg)](. \\\"Pydantic AI\\\")\\n\\n\\n\\n\\nPydantic + AI\\n\\nPydantic AI\\n\\n\\n\\n\\n\\n\\n\\nType to start searching\\n\\n[pydantic/pydantic-ai](https://github.com/pydantic/pydantic-ai + \\\"Go to repository\\\")\\n\\n[![logo](img/logo-white.svg)](. \\\"Pydantic AI\\\")\\nPydantic AI\\n\\n[pydantic/pydantic-ai](https://github.com/pydantic/pydantic-ai + \\\"Go to repository\\\")\\n\\n* Pydantic AI\\n\\n [Pydantic AI](.)\\n\\n\\n\\n Table of contents\\n + [Why use + Pydantic AI](#why-use-pydantic-ai)\\n + [Hello World Example](#hello-world-example)\\n + [Tools & Dependency Injection + Example](#tools-dependency-injection-example)\\n + [Instrumentation with Pydantic Logfire](#instrumentation-with-pydantic-logfire)\\n + \ + [llms.txt](#llmstxt)\\n + [Next Steps](#next-steps)\\n* [Installation](install/)\\n* [Getting Help](help/)\\n* + [Troubleshooting](troubleshooting/)\\n* [Pydantic AI Gateway](gateway/)\\n* Documentation\\n\\n\\n\\n\\n Documentation\\n + \ + Core Concepts\\n\\n\\n\\n\\n Core Concepts\\n - [Agents](agents/)\\n - [Dependencies](dependencies/)\\n + \ - [Function Tools](tools/)\\n - [Output](output/)\\n - [Messages and chat history](message-history/)\\n + \ - [Direct Model Requests](direct/)\\n + Models & Providers\\n\\n\\n\\n\\n Models & Providers\\n - [Overview](models/overview/)\\n + \ - [OpenAI](models/openai/)\\n - [Anthropic](models/anthropic/)\\n - [Google](models/google/)\\n - [Bedrock](models/bedrock/)\\n + \ - [Cohere](models/cohere/)\\n - [Groq](models/groq/)\\n - [Hugging Face](models/huggingface/)\\n - [Mistral](models/mistral/)\\n + \ - [OpenRouter](models/openrouter/)\\n - [Outlines](models/outlines/)\\n + Tools & Toolsets\\n\\n\\n\\n\\n + \ Tools & Toolsets\\n - [Function Tools](tools/)\\n - [Advanced Tool Features](tools-advanced/)\\n - [Toolsets](toolsets/)\\n + \ - [Deferred Tools](deferred-tools/)\\n - [Built-in Tools](builtin-tools/)\\n - [Common Tools](common-tools/)\\n + \ - [Third-Party Tools](third-party-tools/)\\n + Advanced Features\\n\\n\\n\\n\\n Advanced Features\\n - + [Image, Audio, Video & Document Input](input/)\\n - [Thinking](thinking/)\\n - [HTTP Request Retries](retries/)\\n + \ + MCP\\n\\n\\n\\n\\n MCP\\n - [Overview](mcp/overview/)\\n - [Client](mcp/client/)\\n - [FastMCP Client](mcp/fastmcp-client/)\\n + \ - [Server](mcp/server/)\\n + [Multi-Agent Patterns](multi-agent-applications/)\\n + [Testing](testing/)\\n* + Pydantic Evals\\n\\n\\n\\n\\n Pydantic Evals\\n + [Overview](evals/)\\n + Getting Started\\n\\n\\n\\n\\n Getting + Started\\n - [Quick Start](evals/quick-start/)\\n - [Core Concepts](evals/core-concepts/)\\n + Evaluators\\n\\n\\n\\n\\n + \ Evaluators\\n - [Overview](evals/evaluators/overview/)\\n - [Built-in Evaluators](evals/evaluators/built-in/)\\n + \ - [LLM Judge](evals/evaluators/llm-judge/)\\n - [Custom Evaluators](evals/evaluators/custom/)\\n - [Span-Based](evals/evaluators/span-based/)\\n + \ + How-To Guides\\n\\n\\n\\n\\n How-To Guides\\n - [Logfire Integration](evals/how-to/logfire-integration/)\\n + \ - [Dataset Management](evals/how-to/dataset-management/)\\n - [Dataset Serialization](evals/how-to/dataset-serialization/)\\n + \ - [Concurrency & Performance](evals/how-to/concurrency/)\\n - [Retry Strategies](evals/how-to/retry-strategies/)\\n + \ - [Metrics & Attributes](evals/how-to/metrics-attributes/)\\n + Examples\\n\\n\\n\\n\\n Examples\\n - [Simple + Validation](evals/examples/simple-validation/)\\n* Pydantic Graph\\n\\n\\n\\n\\n Pydantic Graph\\n + [Overview](graph/)\\n + \ + [Beta API](graph/beta/)\\n\\n Beta API\\n - [Steps](graph/beta/steps/)\\n - [Joins & Reducers](graph/beta/joins/)\\n + \ - [Decisions](graph/beta/decisions/)\\n - [Parallel Execution](graph/beta/parallel/)\\n* Integrations\\n\\n\\n\\n\\n + \ Integrations\\n + [Debugging & Monitoring with Pydantic Logfire](logfire/)\\n + Durable Execution\\n\\n\\n\\n\\n + \ Durable Execution\\n - [Overview](durable_execution/overview/)\\n - [Temporal](durable_execution/temporal/)\\n + \ - [DBOS](durable_execution/dbos/)\\n - [Prefect](durable_execution/prefect/)\\n + UI Event Streams\\n\\n\\n\\n\\n + \ UI Event Streams\\n - [Overview](ui/overview/)\\n - [AG-UI](ui/ag-ui/)\\n - [Vercel AI](ui/vercel-ai/)\\n + \ + [Agent2Agent (A2A)](a2a/)\\n* Related Packages\\n\\n\\n\\n\\n Related Packages\\n + [Clai](cli/)\\n* Examples\\n\\n\\n\\n\\n + \ Examples\\n + [Setup](examples/setup/)\\n + Getting Started\\n\\n\\n\\n\\n Getting Started\\n - [Pydantic + Model](examples/pydantic-model/)\\n - [Weather agent](examples/weather-agent/)\\n + Conversational Agents\\n\\n\\n\\n\\n + \ Conversational Agents\\n - [Chat App with FastAPI](examples/chat-app/)\\n - [Bank support](examples/bank-support/)\\n + \ + Data & Analytics\\n\\n\\n\\n\\n Data & Analytics\\n - [SQL Generation](examples/sql-gen/)\\n - [Data + Analyst](examples/data-analyst/)\\n - [RAG](examples/rag/)\\n + Streaming\\n\\n\\n\\n\\n Streaming\\n - + [Stream markdown](examples/stream-markdown/)\\n - [Stream whales](examples/stream-whales/)\\n + Complex Workflows\\n\\n\\n\\n\\n + \ Complex Workflows\\n - [Flight booking](examples/flight-booking/)\\n - [Question Graph](examples/question-graph/)\\n + \ + Business Applications\\n\\n\\n\\n\\n Business Applications\\n - [Slack Lead Qualifier with Modal](examples/slack-lead-qualifier/)\\n + \ + UI Examples\\n\\n\\n\\n\\n UI Examples\\n - [Agent User Interaction (AG-UI)](examples/ag-ui/)\\n* API Reference\\n\\n\\n\\n\\n + \ API Reference\\n + pydantic\\\\_ai\\n\\n\\n\\n\\n pydantic\\\\_ai\\n - [pydantic\\\\_ai.agent](api/agent/)\\n + \ - [pydantic\\\\_ai.tools](api/tools/)\\n - [pydantic\\\\_ai.toolsets](api/toolsets/)\\n - [pydantic\\\\_ai.builtin\\\\_tools](api/builtin_tools/)\\n + \ - [pydantic\\\\_ai.common\\\\_tools](api/common_tools/)\\n - [pydantic\\\\_ai.durable\\\\_exec](api/durable_exec/)\\n + \ - [pydantic\\\\_ai.output](api/output/)\\n - [pydantic\\\\_ai.result](api/result/)\\n - [pydantic\\\\_ai.messages](api/messages/)\\n + \ - [pydantic\\\\_ai.exceptions](api/exceptions/)\\n - [pydantic\\\\_ai.settings](api/settings/)\\n - [pydantic\\\\_ai.usage](api/usage/)\\n + \ - [pydantic\\\\_ai.mcp](api/mcp/)\\n - [pydantic\\\\_ai.format\\\\_prompt](api/format_prompt/)\\n - [pydantic\\\\_ai.direct](api/direct/)\\n + \ - [pydantic\\\\_ai.ext](api/ext/)\\n - [pydantic\\\\_ai.models.anthropic](api/models/anthropic/)\\n - [pydantic\\\\_ai.models](api/models/base/)\\n + \ - [pydantic\\\\_ai.models.bedrock](api/models/bedrock/)\\n - [pydantic\\\\_ai.models.cohere](api/models/cohere/)\\n + \ - [pydantic\\\\_ai.models.fallback](api/models/fallback/)\\n - [pydantic\\\\_ai.models.function](api/models/function/)\\n + \ - [pydantic\\\\_ai.models.google](api/models/google/)\\n - [pydantic\\\\_ai.models.groq](api/models/groq/)\\n + \ - [pydantic\\\\_ai.models.huggingface](api/models/huggingface/)\\n - [pydantic\\\\_ai.models.instrumented](api/models/instrumented/)\\n + \ - [pydantic\\\\_ai.models.mcp\\\\_sampling](api/models/mcp-sampling/)\\n - [pydantic\\\\_ai.models.mistral](api/models/mistral/)\\n + \ - [pydantic\\\\_ai.models.openai](api/models/openai/)\\n - [pydantic\\\\_ai.models.openrouter](api/models/openrouter/)\\n + \ - [pydantic\\\\_ai.models.outlines](api/models/outlines/)\\n - [pydantic\\\\_ai.models.test](api/models/test/)\\n + \ - [pydantic\\\\_ai.models.wrapper](api/models/wrapper/)\\n - [pydantic\\\\_ai.profiles](api/profiles/)\\n - + [pydantic\\\\_ai.providers](api/providers/)\\n - [pydantic\\\\_ai.retries](api/retries/)\\n - [pydantic\\\\_ai.run](api/run/)\\n + \ - [pydantic\\\\_ai.ag\\\\_ui](api/ag_ui/)\\n - [pydantic\\\\_ai.ui](api/ui/base/)\\n - [pydantic\\\\_ai.ui.ag\\\\_ui](api/ui/ag_ui/)\\n + \ - [pydantic\\\\_ai.ui.vercel\\\\_ai](api/ui/vercel_ai/)\\n + pydantic\\\\_evals\\n\\n\\n\\n\\n pydantic\\\\_evals\\n + \ - [pydantic\\\\_evals.dataset](api/pydantic_evals/dataset/)\\n - [pydantic\\\\_evals.evaluators](api/pydantic_evals/evaluators/)\\n + \ - [pydantic\\\\_evals.reporting](api/pydantic_evals/reporting/)\\n - [pydantic\\\\_evals.otel](api/pydantic_evals/otel/)\\n + \ - [pydantic\\\\_evals.generation](api/pydantic_evals/generation/)\\n + pydantic\\\\_graph\\n\\n\\n\\n\\n pydantic\\\\_graph\\n + \ - [pydantic\\\\_graph](api/pydantic_graph/graph/)\\n - [pydantic\\\\_graph.nodes](api/pydantic_graph/nodes/)\\n + \ - [pydantic\\\\_graph.persistence](api/pydantic_graph/persistence/)\\n - [pydantic\\\\_graph.mermaid](api/pydantic_graph/mermaid/)\\n + \ - [pydantic\\\\_graph.exceptions](api/pydantic_graph/exceptions/)\\n - Beta API\\n\\n\\n\\n\\n Beta API\\n + \ * [pydantic\\\\_graph.beta](api/pydantic_graph/beta/)\\n * [pydantic\\\\_graph.beta.graph](api/pydantic_graph/beta_graph/)\\n + \ * [pydantic\\\\_graph.beta.graph\\\\_builder](api/pydantic_graph/beta_graph_builder/)\\n * [pydantic\\\\_graph.beta.step](api/pydantic_graph/beta_step/)\\n + \ * [pydantic\\\\_graph.beta.join](api/pydantic_graph/beta_join/)\\n * [pydantic\\\\_graph.beta.decision](api/pydantic_graph/beta_decision/)\\n + \ * [pydantic\\\\_graph.beta.node](api/pydantic_graph/beta_node/)\\n + fasta2a\\n\\n\\n\\n\\n fasta2a\\n - + [fasta2a](api/fasta2a/)\\n* Project\\n\\n\\n\\n\\n Project\\n + [Contributing](contributing/)\\n + [Upgrade Guide](changelog/)\\n + \ + [Version policy](version-policy/)\\n\\nTable of contents\\n\\n* [Why use Pydantic AI](#why-use-pydantic-ai)\\n* + [Hello World Example](#hello-world-example)\\n* [Tools & Dependency Injection Example](#tools-dependency-injection-example)\\n* + [Instrumentation with Pydantic Logfire](#instrumentation-with-pydantic-logfire)\\n* [llms.txt](#llmstxt)\\n* [Next + Steps](#next-steps)\\n\\n# Pydantic AI\\n\\n![Pydantic AI](./img/pydantic-ai-dark.svg#only-dark)\\n\\n![Pydantic AI](./img/pydantic-ai-light.svg#only-light)\\n\\n*GenAI + Agent Framework, the Pydantic way*\\n\\n[![CI](https://github.com/pydantic/pydantic-ai/actions/workflows/ci.yml/badge.svg?event=push)](https://github.com/pydantic/pydantic-ai/actions/workflows/ci.yml?query=branch%3Amain)\\n[![Coverage](https://coverage-badge.samuelcolvin.workers.dev/pydantic/pydantic-ai.svg)](https://coverage-badge.samuelcolvin.workers.dev/redirect/pydantic/pydantic-ai)\\n[![PyPI](https://img.shields.io/pypi/v/pydantic-ai.svg)](https://pypi.python.org/pypi/pydantic-ai)\\n[![versions](https://img.shields.io/pypi/pyversions/pydantic-ai.svg)](https://github.com/pydantic/pydantic-ai)\\n[![license](https://img.shields.io/github/license/pydantic/pydantic-ai.svg)](https://github.com/pydantic/pydantic-ai/blob/main/LICENSE)\\n[![Join + Slack](https://img.shields.io/badge/Slack-Join%20Slack-4A154B?logo=slack)](https://logfire.pydantic.dev/docs/join-slack/)\\n\\nPydantic + AI is a Python agent framework designed to help you\\nquickly, confidently, and painlessly build production grade + applications and workflows with Generative AI.\\n\\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.\\n\\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.\\n\\nWe built Pydantic AI with one simple aim: to bring that FastAPI feeling to GenAI app and + agent development.\\n\\n## Why use Pydantic AI\\n\\n1. **Built 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?* ![\U0001F603](https://cdn.jsdelivr.net/gh/jdecked/twemoji@15.1.0/assets/svg/1f603.svg + \\\":smiley:\\\")\\n2. **Model-agnostic**:\\n 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).\\n3. **Seamless Observability**:\\n 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).\\n4. **Fully Type-safe**:\\n + \ 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.\\n5. **Powerful Evals**:\\n 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.\\n6. **MCP, + A2A, and UI**:\\n 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.\\n7. **Human-in-the-Loop Tool + Approval**:\\n 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.\\n8. + **Durable Execution**:\\n 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.\\n9. **Streamed Outputs**:\\n Provides the ability + to [stream](output/#streamed-results) structured output continuously, with immediate validation, ensuring real time + access to generated data.\\n10. **Graph Support**:\\n 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.\\n\\nRealistically + though, no list is going to be as convincing as [giving it a try](#next-steps) and seeing how it makes you feel!\\n\\n**Sign + up for our newsletter, *The Pydantic Stack*, with updates & tutorials on Pydantic AI, Logfire, and Pydantic:**\\n\\nSubscribe\\n\\n## + Hello World Example\\n\\nHere's a minimal example of Pydantic AI:\\n\\nWith Pydantic AI GatewayDirectly to Provider + API\\n\\n[Learn about Gateway](../gateway) hello\\\\_world.py\\n\\n```\\nfrom pydantic_ai import Agent\\n\\nagent + = Agent( # (1)!\\n 'gateway/anthropic:claude-sonnet-4-0',\\n instructions='Be concise, reply with one sentence.', + \ # (2)!\\n)\\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```\\n\\n1. + 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.\\n2. Register static [instructions](agents/#instructions) using a keyword argument to + the agent.\\n3. [Run the agent](agents/#running-agents) synchronously, starting a conversation with the LLM.\\n\\nhello\\\\_world.py\\n\\n```\\nfrom + pydantic_ai import Agent\\n\\nagent = Agent( # (1)!\\n 'anthropic:claude-sonnet-4-0',\\n instructions='Be concise, + reply with one sentence.', # (2)!\\n)\\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```\\n\\n1. 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.\\n2. Register static [instructions](agents/#instructions) + using a keyword argument to the agent.\\n3. [Run the agent](agents/#running-agents) synchronously, starting a conversation + with the LLM.\\n\\n*(This example is complete, it can be run \\\"as is\\\", assuming you've [installed the `pydantic_ai` + package](install/))*\\n\\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.\\n\\nNot very interesting yet, but we can easily add [tools](tools/), + [dynamic instructions](agents/#instructions), and [structured outputs](output/) to build more powerful agents.\\n\\n## + Tools & Dependency Injection Example\\n\\nHere is a concise example using Pydantic AI to build a support agent for + a bank:\\n\\nWith Pydantic AI GatewayDirectly to Provider API\\n\\n[Learn about Gateway](../gateway) bank\\\\_support.py\\n\\n```\\nfrom + dataclasses import dataclass\\n\\nfrom pydantic import BaseModel, Field\\nfrom pydantic_ai import Agent, RunContext\\n\\nfrom + bank_database import DatabaseConn\\n\\n\\n@dataclass\\nclass SupportDependencies: # (3)!\\n customer_id: int\\n + \ db: DatabaseConn # (12)!\\n\\n\\nclass SupportOutput(BaseModel): # (13)!\\n support_advice: str = Field(description='Advice + returned to the customer')\\n block_card: bool = Field(description=\\\"Whether to block the customer's card\\\")\\n + \ risk: int = Field(description='Risk level of query', ge=0, le=10)\\n\\n\\nsupport_agent = Agent( # (1)!\\n 'gateway/openai:gpt-5', + \ # (2)!\\n deps_type=SupportDependencies,\\n output_type=SupportOutput, # (9)!\\n instructions=( # (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\\n\\n@support_agent.instructions # (5)!\\nasync def add_customer_name(ctx: RunContext[SupportDependencies]) + -> str:\\n customer_name = await ctx.deps.db.customer_name(id=ctx.deps.customer_id)\\n return f\\\"The customer's + name is {customer_name!r}\\\"\\n\\n\\n@support_agent.tool # (6)!\\nasync def customer_balance(\\n ctx: RunContext[SupportDependencies], + include_pending: bool\\n) -> float:\\n \\\"\\\"\\\"Returns the customer's current account balance.\\\"\\\"\\\" + \ # (7)!\\n return await ctx.deps.db.customer_balance(\\n id=ctx.deps.customer_id,\\n include_pending=include_pending,\\n + \ )\\n\\n\\n... # (11)!\\n\\n\\nasync def main():\\n deps = SupportDependencies(customer_id=123, db=DatabaseConn())\\n + \ result = await support_agent.run('What is my balance?', deps=deps) # (8)!\\n print(result.output) # (10)!\\n + \ \\\"\\\"\\\"\\n support_advice='Hello John, your current account balance, including pending transactions, is + $123.45.' block_card=False risk=1\\n \\\"\\\"\\\"\\n\\n result = await support_agent.run('I just lost my card!', + deps=deps)\\n print(result.output)\\n \\\"\\\"\\\"\\n support_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```\\n\\n1. + 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 type `Agent[SupportDependencies, SupportOutput]`.\\n2. + 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.\\n3. 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.\\n4. Static [instructions](agents/#instructions) + can be registered with the [`instructions` keyword argument](api/agent/#pydantic_ai.agent.Agent.__init__) to the agent.\\n5. + Dynamic [instructions](agents/#instructions) can be registered with the [`@agent.instructions`](api/agent/#pydantic_ai.agent.Agent.instructions) + decorator, and can make use of dependency injection. Dependencies are carried via the [`RunContext`](api/tools/#pydantic_ai.tools.RunContext) + argument, which is parameterized with the `deps_type` from above. If the type annotation here is wrong, static type + checkers will catch it.\\n6. The [`@agent.tool`](tools/) decorator let you register functions which the LLM may call + while responding to a user. Again, dependencies are carried via [`RunContext`](api/tools/#pydantic_ai.tools.RunContext), + 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.\\n7. 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.\\n8. [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.\\n9. 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.\\n10. The output will be validated with Pydantic to guarantee it is a `SupportOutput`, + since the agent is generic, it'll also be typed as a `SupportOutput` to aid with static type checking.\\n11. 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.\\n12. 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.\\n13. + 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.\\n\\nbank\\\\_support.py\\n\\n```\\nfrom dataclasses import + dataclass\\n\\nfrom pydantic import BaseModel, Field\\nfrom pydantic_ai import Agent, RunContext\\n\\nfrom bank_database + import DatabaseConn\\n\\n\\n@dataclass\\nclass SupportDependencies: # (3)!\\n customer_id: int\\n db: DatabaseConn + \ # (12)!\\n\\n\\nclass SupportOutput(BaseModel): # (13)!\\n support_advice: str = Field(description='Advice returned + to the customer')\\n block_card: bool = Field(description=\\\"Whether to block the customer's card\\\")\\n risk: + int = Field(description='Risk level of query', ge=0, le=10)\\n\\n\\nsupport_agent = Agent( # (1)!\\n 'openai:gpt-5', + \ # (2)!\\n deps_type=SupportDependencies,\\n output_type=SupportOutput, # (9)!\\n instructions=( # (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\\n\\n@support_agent.instructions # (5)!\\nasync def add_customer_name(ctx: RunContext[SupportDependencies]) + -> str:\\n customer_name = await ctx.deps.db.customer_name(id=ctx.deps.customer_id)\\n return f\\\"The customer's + name is {customer_name!r}\\\"\\n\\n\\n@support_agent.tool # (6)!\\nasync def customer_balance(\\n ctx: RunContext[SupportDependencies], + include_pending: bool\\n) -> float:\\n \\\"\\\"\\\"Returns the customer's current account balance.\\\"\\\"\\\" + \ # (7)!\\n return await ctx.deps.db.customer_balance(\\n id=ctx.deps.customer_id,\\n include_pending=include_pending,\\n + \ )\\n\\n\\n... # (11)!\\n\\n\\nasync def main():\\n deps = SupportDependencies(customer_id=123, db=DatabaseConn())\\n + \ result = await support_agent.run('What is my balance?', deps=deps) # (8)!\\n print(result.output) # (10)!\\n + \ \\\"\\\"\\\"\\n support_advice='Hello John, your current account balance, including pending transactions, is + $123.45.' block_card=False risk=1\\n \\\"\\\"\\\"\\n\\n result = await support_agent.run('I just lost my card!', + deps=deps)\\n print(result.output)\\n \\\"\\\"\\\"\\n support_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```\\n\\n1. + 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 type `Agent[SupportDependencies, SupportOutput]`.\\n2. + 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.\\n3. 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.\\n4. Static [instructions](agents/#instructions) + can be registered with the [`instructions` keyword argument](api/agent/#pydantic_ai.agent.Agent.__init__) to the agent.\\n5. + Dynamic [instructions](agents/#instructions) can be registered with the [`@agent.instructions`](api/agent/#pydantic_ai.agent.Agent.instructions) + decorator, and can make use of dependency injection. Dependencies are carried via the [`RunContext`](api/tools/#pydantic_ai.tools.RunContext) + argument, which is parameterized with the `deps_type` from above. If the type annotation here is wrong, static type + checkers will catch it.\\n6. The [`@agent.tool`](tools/) decorator let you register functions which the LLM may call + while responding to a user. Again, dependencies are carried via [`RunContext`](api/tools/#pydantic_ai.tools.RunContext), + 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.\\n7. 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.\\n8. [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.\\n9. 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.\\n10. The output will be validated with Pydantic to guarantee it is a `SupportOutput`, + since the agent is generic, it'll also be typed as a `SupportOutput` to aid with static type checking.\\n11. 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.\\n12. 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.\\n13. + 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.\\n\\nComplete `bank_support.py` example\\n\\nThe 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/).\\n\\n## Instrumentation with Pydantic Logfire\\n\\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.\\nTo understand the flow of the above runs, we can + watch the agent in action using Pydantic Logfire.\\n\\nTo do this, we need to [set up Logfire](logfire/#using-logfire), + and add the following to our code:\\n\\nWith Pydantic AI GatewayDirectly to Provider API\\n\\n[Learn about Gateway](../gateway) + bank\\\\_support\\\\_with\\\\_logfire.py\\n\\n```\\n...\\nfrom pydantic_ai import Agent, RunContext\\n\\nfrom bank_database + import DatabaseConn\\n\\nimport logfire\\n\\nlogfire.configure() # (1)!\\nlogfire.instrument_pydantic_ai() # (2)!\\nlogfire.instrument_asyncpg() + \ # (3)!\\n\\n...\\n\\nsupport_agent = Agent(\\n 'gateway/openai:gpt-5',\\n deps_type=SupportDependencies,\\n + \ output_type=SupportOutput,\\n system_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```\\n\\n1. Configure the Logfire + SDK, this will fail if project is not set up.\\n2. 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 [`instrument=True` keyword argument](api/agent/#pydantic_ai.agent.Agent.__init__) + to the agent.\\n3. In our demo, `DatabaseConn` uses `asyncpg` to connect to a PostgreSQL database, so [`logfire.instrument_asyncpg()`](https://magicstack.github.io/asyncpg/current/) + is used to log the database queries.\\n\\nbank\\\\_support\\\\_with\\\\_logfire.py\\n\\n```\\n...\\nfrom pydantic_ai + import Agent, RunContext\\n\\nfrom bank_database import DatabaseConn\\n\\nimport logfire\\n\\nlogfire.configure() + \ # (1)!\\nlogfire.instrument_pydantic_ai() # (2)!\\nlogfire.instrument_asyncpg() # (3)!\\n\\n...\\n\\nsupport_agent + = Agent(\\n 'openai:gpt-5',\\n deps_type=SupportDependencies,\\n output_type=SupportOutput,\\n system_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```\\n\\n1. Configure the Logfire SDK, this will fail if project is not set up.\\n2. 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 [`instrument=True` keyword argument](api/agent/#pydantic_ai.agent.Agent.__init__) to the agent.\\n3. In our + demo, `DatabaseConn` uses `asyncpg` to connect to a PostgreSQL database, so [`logfire.instrument_asyncpg()`](https://magicstack.github.io/asyncpg/current/) + is used to log the database queries.\\n\\nThat's enough to get the following view of your agent in action:\\n\\nSee + [Monitoring and Performance](logfire/) to learn more.\\n\\n## `llms.txt`\\n\\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.\\n\\nTwo formats are available:\\n\\n* [`llms.txt`](https://ai.pydantic.dev/llms.txt): a file + containing a brief description\\n of the project, along with links to the different sections of the documentation. + The structure\\n of this file is described in details [here](https://llmstxt.org/#format).\\n* [`llms-full.txt`](https://ai.pydantic.dev/llms-full.txt): + Similar to the `llms.txt` file,\\n but every link content is included. Note that this file may be too large for some + LLMs.\\n\\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.\\n\\n## Next Steps\\n\\nTo try Pydantic AI for yourself, [install it](install/) + and follow the instructions [in the examples](examples/setup/).\\n\\nRead the [docs](agents/) to learn more about + building applications with Pydantic AI.\\n\\nRead the [API Reference](api/agent/) to understand Pydantic AI's interface.\\n\\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.\\n\\n\\n\\n© Pydantic Services Inc. 2024 to present\"},\"title\":\"Pydantic AI\"}}} }\n\nevent: + content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":2 }\n\nevent: content_block_start\ndata: + {\"type\":\"content_block_start\",\"index\":3,\"content_block\":{\"type\":\"text\",\"text\":\"\"} }\n\nevent: + content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":3,\"delta\":{\"type\":\"text_delta\",\"text\":\"\\\"\"} + \ }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":3,\"delta\":{\"type\":\"text_delta\",\"text\":\"Gen\"} + \ }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":3,\"delta\":{\"type\":\"text_delta\",\"text\":\"A\"} + \ }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":3,\"delta\":{\"type\":\"text_delta\",\"text\":\"I + Agent\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":3,\"delta\":{\"type\":\"text_delta\",\"text\":\" + Framework\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":3,\"delta\":{\"type\":\"text_delta\",\"text\":\",\"} + \ }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":3,\"delta\":{\"type\":\"text_delta\",\"text\":\" + the\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":3,\"delta\":{\"type\":\"text_delta\",\"text\":\" + Pydantic way\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":3,\"delta\":{\"type\":\"text_delta\",\"text\":\"\\\"\"} + \ }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":3 }\n\nevent: message_delta\ndata: + {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null},\"usage\":{\"input_tokens\":11453,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":151,\"server_tool_use\":{\"web_search_requests\":0,\"web_fetch_requests\":1}} + \ }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n" headers: cache-control: - no-cache @@ -199,7 +358,7 @@ interactions: content-type: - text/event-stream; charset=utf-8 retry-after: - - '15' + - '46' strict-transport-security: - max-age=31536000; includeSubDomains; preload transfer-encoding: @@ -208,4 +367,3 @@ interactions: code: 200 message: OK version: 1 -... diff --git a/tests/models/cassettes/test_google/test_google_model_web_search_tool.yaml b/tests/models/cassettes/test_google/test_google_model_web_search_tool.yaml index 3da8a73955..929a05e527 100644 --- a/tests/models/cassettes/test_google/test_google_model_web_search_tool.yaml +++ b/tests/models/cassettes/test_google/test_google_model_web_search_tool.yaml @@ -8,7 +8,7 @@ interactions: connection: - keep-alive content-length: - - '247' + - '277' content-type: - application/json host: @@ -19,7 +19,9 @@ interactions: - parts: - text: What is the weather in San Francisco today? role: user - generationConfig: {} + generationConfig: + responseModalities: + - TEXT systemInstruction: parts: - text: You are a helpful chatbot. @@ -32,11 +34,11 @@ interactions: alt-svc: - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 content-length: - - '9827' + - '11738' content-type: - application/json; charset=UTF-8 server-timing: - - gfet4t7; dur=8115 + - gfet4t7; dur=9106 transfer-encoding: - chunked vary: @@ -48,15 +50,13 @@ interactions: - content: parts: - text: |- - ## Weather in San Francisco is Mild and Partly Cloudy + **San Francisco Experiences a Mostly Cloudy Day with Mild Temperatures** - **San Francisco, CA** - Residents and visitors in San Francisco are experiencing a mild Tuesday, with partly cloudy skies and temperatures hovering around 69°F. There is a very low chance of rain throughout the day. + As of Monday in San Francisco, the weather is predominantly mostly cloudy with a current temperature of 51°F, which feels more like 49°F. Other reports describe the sky as partly cloudy. The forecast for the day indicates that cloudy conditions will persist with a slight 10% chance of rain. - According to the latest weather reports, the forecast for the remainder of the day is expected to be sunny, with highs ranging from the mid-60s to the lower 80s. Winds are predicted to come from the west at 10 to 15 mph. + The high temperature for the day is expected to reach the low to mid-50s, with forecasts predicting a high of 54°F. The low for tonight is anticipated to be in the mid-40s, with some forecasts calling for a low of 45°F. - As the evening approaches, the skies are expected to remain partly cloudy, with temperatures dropping to the upper 50s. There is a slight increase in the chance of rain overnight, but it remains low at 20%. - - Overall, today's weather in San Francisco is pleasant, with a mix of sun and clouds and comfortable temperatures. + Humidity levels are relatively high, reported at 77%. Winds are generally light, coming from the north at 5 to 10 mph. Visibility has been noted as being around 1.5 to 2 miles. There is also a possibility of dense fog in some areas. role: model finishReason: STOP groundingMetadata: @@ -65,51 +65,79 @@ interactions: title: Weather information for San Francisco, CA, US uri: https://www.google.com/search?q=weather+in+San Francisco, CA,+US - web: - title: weather.gov - uri: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQF_uqo2G5Goeww8iF1L_dYa2sqWGhzu_UnxEZd1gQ7ZNuXEVVVYEEYcx_La3kuODFm0dPUhHeF4qGP1c6kJ86i4SKfvRqFitMCvNiDx07eC5iM7axwepoTv3FeUdIRC-ou1P-6DDykZ4QzcxcrKISa_1Q== + title: cbsnews.com + uri: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHs0KxAufAKly9TnNAnn-Baxlk1USk6vVjzW1FEEUCZKnE5eeatKXcIXJ4HZNDI7ROs1q3IuHTZp5bZwB3hc-u1sXnXI0rosZTNt-t6H9FTF9owH4M5u-54bnuL01OC1C3fEnU7MNRV - web: title: wunderground.com - uri: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFywixFZicmDjijfhfLNw8ya7XdqWR31aJp8CHyULLelG8bujH1TuqeP9RAhK6Pcm1qz11ujm2yM7gM5bJXDFsZwbsubub4cnUp5ixRaloJcjVrHkyd5RHblhkDDxHGiREV9BcuqeJovdr8qhtrCKMcvJk= + uri: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGIi3F3lZlGrWP6vK_zucR90Esg-PCBYRgHOK0gvSIwbHM1jKGKhLwKbv-chvrUhyhc5IZ9XAjo59IBjM6Tk5p2qQK7QGW-WgFi0Nrus7IrBqgNJfWCAMTgqkde8RXWk-T2edNf90TJGVoJIKXhcuWxpuE= + - web: + title: accuweather.com + uri: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFUEG5HLOGJoFbOTpesMFek1T82haMch6RGzVbNaG-m-B7mbwLkfb4pHpCE8CH_RHUMa5GKNPUxev47neFe9YXiKJdXKo4gMABpBHjb3AjAXg5UNoY3hvMfhBVw-mxSLTzHuRzdxvjgMpDbskeuMVt0jM543GJnG06EVVzeYm8UUemWTS2UJyE= + - web: + title: theweathernetwork.com + uri: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHtG8yKh-VGE2kjUi05sHqdu5HUVFr6NUs9F3SdvzlnHjubasE5fLourWIMk6Wmgd6VFmAj8NcY3HNeFosMwnxMc3zyvlZ2V6gNDMFuGgJCofZD1vlE0t2vv4KFC13o1LnElXVvzht7u-6kfwQybN_57kHs63NjwCQLiYECj8Ub7yDRhCYAzCU= + - web: + title: weather.com + uri: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHggnMLBYdYahOGPJ6dykd1mBx7bs82NTc6T-pndOKmIdb8s-3QQ2571SFt8NPG6_UstE9rYG5g2yegUOlsiREqykqesDB7UP58H3ApEe2hDCMTqSyRllMUusJecf2jVHF8kb2Y1rCBNkzaXKuhtxgx6OPvoaYNnYDhIo73 groundingSupports: - groundingChunkIndices: - 0 segment: - endIndex: 216 - startIndex: 55 - text: '**San Francisco, CA** - Residents and visitors in San Francisco are experiencing a mild Tuesday, with - partly cloudy skies and temperatures hovering around 69°F.' + endIndex: 213 + startIndex: 74 + text: As of Monday in San Francisco, the weather is predominantly mostly cloudy with a current temperature of + 51°F, which feels more like 49°F. - groundingChunkIndices: - - 0 + - 1 segment: - endIndex: 271 - startIndex: 217 - text: There is a very low chance of rain throughout the day. + endIndex: 262 + startIndex: 214 + text: Other reports describe the sky as partly cloudy. - groundingChunkIndices: - - 1 + - 0 segment: - endIndex: 434 - startIndex: 273 - text: According to the latest weather reports, the forecast for the remainder of the day is expected to be sunny, - with highs ranging from the mid-60s to the lower 80s. + endIndex: 367 + startIndex: 263 + text: The forecast for the day indicates that cloudy conditions will persist with a slight 10% chance of rain. - groundingChunkIndices: - 2 + - 3 segment: - endIndex: 493 - startIndex: 435 - text: Winds are predicted to come from the west at 10 to 15 mph. + endIndex: 485 + startIndex: 369 + text: The high temperature for the day is expected to reach the low to mid-50s, with forecasts predicting a + high of 54°F. - groundingChunkIndices: - - 1 + - 3 segment: - endIndex: 614 - startIndex: 495 - text: As the evening approaches, the skies are expected to remain partly cloudy, with temperatures dropping - to the upper 50s. + endIndex: 590 + startIndex: 486 + text: The low for tonight is anticipated to be in the mid-40s, with some forecasts calling for a low of 45°F. - groundingChunkIndices: - 0 segment: - endIndex: 701 - startIndex: 615 - text: There is a slight increase in the chance of rain overnight, but it remains low at 20%. + endIndex: 645 + startIndex: 592 + text: Humidity levels are relatively high, reported at 77%. + - groundingChunkIndices: + - 2 + segment: + endIndex: 710 + startIndex: 646 + text: Winds are generally light, coming from the north at 5 to 10 mph. + - groundingChunkIndices: + - 4 + - 5 + segment: + endIndex: 768 + startIndex: 711 + text: Visibility has been noted as being around 1.5 to 2 miles. + - groundingChunkIndices: + - 2 + segment: + endIndex: 824 + startIndex: 769 + text: There is also a possibility of dense fog in some areas. searchEntryPoint: renderedContent: |