Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pydantic_ai_slim/pydantic_ai/agent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1142,6 +1142,7 @@ def tool_plain(
sequential: bool = False,
requires_approval: bool = False,
metadata: dict[str, Any] | None = None,
input_examples: list[dict[str, Any]] | None = None,
) -> Callable[[ToolFuncPlain[ToolParams]], ToolFuncPlain[ToolParams]]: ...

def tool_plain(
Expand All @@ -1160,6 +1161,7 @@ def tool_plain(
sequential: bool = False,
requires_approval: bool = False,
metadata: dict[str, Any] | None = None,
input_examples: list[dict[str, Any]] | None = None,
) -> Any:
"""Decorator to register a tool function which DOES NOT take `RunContext` as an argument.

Expand Down Expand Up @@ -1209,6 +1211,8 @@ async def spam(ctx: RunContext[str]) -> float:
requires_approval: Whether this tool requires human-in-the-loop approval. Defaults to False.
See the [tools documentation](../deferred-tools.md#human-in-the-loop-tool-approval) for more info.
metadata: Optional metadata for the tool. This is not sent to the model but can be used for filtering and tool behavior customization.
input_examples: Example inputs demonstrating correct tool usage. Defaults to None.
See [`ToolDefinition.input_examples`][pydantic_ai.tools.ToolDefinition.input_examples] for more info.
"""

def tool_decorator(func_: ToolFuncPlain[ToolParams]) -> ToolFuncPlain[ToolParams]:
Expand All @@ -1227,6 +1231,7 @@ def tool_decorator(func_: ToolFuncPlain[ToolParams]) -> ToolFuncPlain[ToolParams
sequential=sequential,
requires_approval=requires_approval,
metadata=metadata,
input_examples=input_examples,
)
return func_

Expand Down
7 changes: 7 additions & 0 deletions pydantic_ai_slim/pydantic_ai/models/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,11 @@ def _get_betas_and_extra_headers(
if has_strict_tools or model_request_parameters.output_mode == 'native':
betas.add('structured-outputs-2025-11-13')

# Check if any tools use input_examples (advanced tool use feature)
has_input_examples = any(tool.get('input_examples') for tool in tools)
if has_input_examples:
betas.add('advanced-tool-use-2025-11-20')
Copy link
Collaborator

Choose a reason for hiding this comment

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

There's a different beta header for Vertex and Bedrock: https://platform.claude.com/docs/en/agents-and-tools/tool-use/implement-tool-use#providing-tool-use-examples

That's relevant when using AnthropicModel with AnthropicProvider and a custom anthropic_client like AsyncAnthropicVertex. So we should check the type of self._client to send the right header.


if beta_header := extra_headers.pop('anthropic-beta', None):
betas.update({stripped_beta for beta in beta_header.split(',') if (stripped_beta := beta.strip())})

Expand Down Expand Up @@ -1041,6 +1046,8 @@ def _map_tool_definition(self, f: ToolDefinition) -> BetaToolParam:
}
if f.strict and self.profile.supports_json_schema_output:
tool_param['strict'] = f.strict
if f.input_examples:
tool_param['input_examples'] = f.input_examples
return tool_param

@staticmethod
Expand Down
18 changes: 18 additions & 0 deletions pydantic_ai_slim/pydantic_ai/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ class Tool(Generic[ToolAgentDepsT]):
sequential: bool
requires_approval: bool
metadata: dict[str, Any] | None
input_examples: list[dict[str, Any]] | None
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should also add this to OutputTool!

function_schema: _function_schema.FunctionSchema
"""
The base JSON schema for the tool's parameters.
Expand All @@ -285,6 +286,7 @@ def __init__(
sequential: bool = False,
requires_approval: bool = False,
metadata: dict[str, Any] | None = None,
input_examples: list[dict[str, Any]] | None = None,
function_schema: _function_schema.FunctionSchema | None = None,
):
"""Create a new tool instance.
Expand Down Expand Up @@ -341,6 +343,8 @@ async def prep_my_tool(
requires_approval: Whether this tool requires human-in-the-loop approval. Defaults to False.
See the [tools documentation](../deferred-tools.md#human-in-the-loop-tool-approval) for more info.
metadata: Optional metadata for the tool. This is not sent to the model but can be used for filtering and tool behavior customization.
input_examples: Example inputs demonstrating correct tool usage. Defaults to None.
See [`ToolDefinition.input_examples`][pydantic_ai.tools.ToolDefinition.input_examples] for more info.
function_schema: The function schema to use for the tool. If not provided, it will be generated.
"""
self.function = function
Expand All @@ -362,6 +366,7 @@ async def prep_my_tool(
self.sequential = sequential
self.requires_approval = requires_approval
self.metadata = metadata
self.input_examples = input_examples

@classmethod
def from_schema(
Expand Down Expand Up @@ -418,6 +423,7 @@ def tool_def(self):
sequential=self.sequential,
metadata=self.metadata,
kind='unapproved' if self.requires_approval else 'function',
input_examples=self.input_examples,
)

async def prepare_tool_def(self, ctx: RunContext[ToolAgentDepsT]) -> ToolDefinition | None:
Expand Down Expand Up @@ -503,6 +509,18 @@ class ToolDefinition:
For MCP tools, this contains the `meta`, `annotations`, and `output_schema` fields from the tool definition.
"""

input_examples: list[dict[str, Any]] | None = None
Copy link
Collaborator

Choose a reason for hiding this comment

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

Anthropic calls them input_examples because they have input_schema, but we have parameters_json_schema, so let's call it either parameters_examples or, better, just examples.

"""Example inputs demonstrating correct tool usage patterns.

Provide 1-5 realistic examples showing parameter conventions, optional field patterns,
nested structures, and API-specific conventions. Each example must validate against
the tool's `parameters_json_schema`.

Supported by:

* [Anthropic](https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/tool-use-examples)
"""

@property
def defer(self) -> bool:
"""Whether calls to this tool will be deferred.
Expand Down
9 changes: 9 additions & 0 deletions pydantic_ai_slim/pydantic_ai/toolsets/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ def tool(
sequential: bool | None = None,
requires_approval: bool | None = None,
metadata: dict[str, Any] | None = None,
input_examples: list[dict[str, Any]] | None = None,
) -> Callable[[ToolFuncEither[AgentDepsT, ToolParams]], ToolFuncEither[AgentDepsT, ToolParams]]: ...

def tool(
Expand All @@ -137,6 +138,7 @@ def tool(
sequential: bool | None = None,
requires_approval: bool | None = None,
metadata: dict[str, Any] | None = None,
input_examples: list[dict[str, Any]] | None = None,
) -> Any:
"""Decorator to register a tool function which takes [`RunContext`][pydantic_ai.tools.RunContext] as its first argument.

Expand Down Expand Up @@ -193,6 +195,8 @@ async def spam(ctx: RunContext[str], y: float) -> float:
If `None`, the default value is determined by the toolset.
metadata: Optional metadata for the tool. This is not sent to the model but can be used for filtering and tool behavior customization.
If `None`, the default value is determined by the toolset. If provided, it will be merged with the toolset's metadata.
input_examples: Example inputs demonstrating correct tool usage. Defaults to None.
See [`ToolDefinition.input_examples`][pydantic_ai.tools.ToolDefinition.input_examples] for more info.
"""

def tool_decorator(
Expand All @@ -213,6 +217,7 @@ def tool_decorator(
sequential=sequential,
requires_approval=requires_approval,
metadata=metadata,
input_examples=input_examples,
)
return func_

Expand All @@ -233,6 +238,7 @@ def add_function(
sequential: bool | None = None,
requires_approval: bool | None = None,
metadata: dict[str, Any] | None = None,
input_examples: list[dict[str, Any]] | None = None,
) -> None:
"""Add a function as a tool to the toolset.

Expand Down Expand Up @@ -267,6 +273,8 @@ def add_function(
If `None`, the default value is determined by the toolset.
metadata: Optional metadata for the tool. This is not sent to the model but can be used for filtering and tool behavior customization.
If `None`, the default value is determined by the toolset. If provided, it will be merged with the toolset's metadata.
input_examples: Example inputs demonstrating correct tool usage. Defaults to None.
See [`ToolDefinition.input_examples`][pydantic_ai.tools.ToolDefinition.input_examples] for more info.
"""
if docstring_format is None:
docstring_format = self.docstring_format
Expand Down Expand Up @@ -295,6 +303,7 @@ def add_function(
sequential=sequential,
requires_approval=requires_approval,
metadata=metadata,
input_examples=input_examples,
)
self.add_tool(tool)

Expand Down
149 changes: 149 additions & 0 deletions tests/models/anthropic/test_input_examples.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
"""Tests for Anthropic input_examples feature on ToolDefinition.

This feature allows providing example inputs to help the model understand
correct tool usage patterns.
"""

from __future__ import annotations

from typing import Any, cast

import pytest

from pydantic_ai import Agent, Tool
from pydantic_ai.tools import ToolDefinition

from ...conftest import try_import
from ..test_anthropic import MockAnthropic, completion_message

with try_import() as imports_successful:
from anthropic.types.beta import BetaTextBlock, BetaUsage

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'),
]


class TestToolDefinitionInputExamples:
"""Tests for ToolDefinition.input_examples field."""

def test_input_examples_defaults_to_none(self):
"""Test that input_examples defaults to None."""
tool_def = ToolDefinition(name='test_tool')
assert tool_def.input_examples is None

def test_input_examples_can_be_set(self):
"""Test setting input_examples."""
examples = [
{'param1': 'value1', 'param2': 123},
{'param1': 'value2'},
]
tool_def = ToolDefinition(name='test_tool', input_examples=examples)
assert tool_def.input_examples == examples


class TestToolInputExamples:
"""Tests for Tool class with input_examples."""

def test_tool_input_examples_default(self):
"""Test that Tool.input_examples defaults to None."""

def my_tool(x: int) -> str:
return str(x)

tool = Tool(my_tool)
assert tool.input_examples is None
assert tool.tool_def.input_examples is None

def test_tool_input_examples_set(self):
"""Test setting Tool.input_examples."""

def my_tool(x: int) -> str:
return str(x)

examples = [{'x': 1}, {'x': 42}]
tool = Tool(my_tool, input_examples=examples)
assert tool.input_examples == examples
assert tool.tool_def.input_examples == examples


class TestAnthropicMapToolDefinition:
"""Tests for AnthropicModel._map_tool_definition with input_examples."""

def test_map_tool_definition_without_input_examples(self):
"""Test tool definition mapping without input_examples."""
tool_def = ToolDefinition(
name='test_tool',
description='A test tool',
parameters_json_schema={'type': 'object', 'properties': {'x': {'type': 'integer'}}},
)
c = completion_message(
[BetaTextBlock(text='Hello', type='text')],
BetaUsage(input_tokens=5, output_tokens=10),
)
mock_client = MockAnthropic.create_mock(c)
model = AnthropicModel('claude-sonnet-4-5', provider=AnthropicProvider(anthropic_client=mock_client))
result = model._map_tool_definition(tool_def) # pyright: ignore[reportPrivateUsage]
result_dict = cast(dict[str, Any], result)
assert result_dict['name'] == 'test_tool'
assert result_dict['description'] == 'A test tool'
assert 'input_examples' not in result_dict

def test_map_tool_definition_with_input_examples(self):
"""Test tool definition mapping with input_examples."""
examples = [{'x': 1}, {'x': 2}]
tool_def = ToolDefinition(
name='test_tool',
description='A test tool',
input_examples=examples,
)
c = completion_message(
[BetaTextBlock(text='Hello', type='text')],
BetaUsage(input_tokens=5, output_tokens=10),
)
mock_client = MockAnthropic.create_mock(c)
model = AnthropicModel('claude-sonnet-4-5', provider=AnthropicProvider(anthropic_client=mock_client))
result = model._map_tool_definition(tool_def) # pyright: ignore[reportPrivateUsage]
result_dict = cast(dict[str, Any], result)
assert result_dict['input_examples'] == examples


class TestAgentWithInputExamples:
"""Tests for Agent with input_examples on tools."""

def test_agent_with_input_examples_tool(self):
"""Test creating an agent with a tool that has input_examples."""

def my_tool(x: int) -> str:
"""A test tool."""
return str(x)

examples = [{'x': 1}, {'x': 42}]
agent = Agent(
'test',
tools=[Tool(my_tool, input_examples=examples)],
)

# Verify the tool was registered with input_examples
tool = agent._function_toolset.tools.get('my_tool') # pyright: ignore[reportPrivateUsage]
assert tool is not None
assert tool.input_examples == examples

def test_agent_tool_plain_decorator_with_input_examples(self):
"""Test the @agent.tool_plain decorator with input_examples."""
agent: Agent[None, str] = Agent('test')

examples = [{'x': 1}, {'x': 42}]

@agent.tool_plain(input_examples=examples)
def my_example_tool(x: int) -> str:
"""A tool with examples."""
return str(x)

tool = agent._function_toolset.tools.get('my_example_tool') # pyright: ignore[reportPrivateUsage]
assert tool is not None
assert tool.input_examples == examples
2 changes: 2 additions & 0 deletions tests/models/test_model_request_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ def test_model_request_parameters_are_serializable():
'sequential': False,
'kind': 'function',
'metadata': None,
'input_examples': None,
}
],
'builtin_tools': [
Expand Down Expand Up @@ -131,6 +132,7 @@ def test_model_request_parameters_are_serializable():
'sequential': False,
'kind': 'function',
'metadata': None,
'input_examples': None,
}
],
'prompted_output_template': None,
Expand Down
2 changes: 2 additions & 0 deletions tests/test_logfire.py
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,7 @@ async def my_ret(x: int) -> str:
'sequential': False,
'kind': 'function',
'metadata': None,
'input_examples': None,
}
],
'builtin_tools': [],
Expand Down Expand Up @@ -994,6 +995,7 @@ class MyOutput:
'sequential': False,
'kind': 'output',
'metadata': None,
'input_examples': None,
}
],
'prompted_output_template': None,
Expand Down
Loading
Loading