Skip to content

Commit 88bf6d3

Browse files
committed
Merge branch 'add_tool_timeout' of https://github.com/DEENUU1/pydantic-ai into add_tool_timeout
2 parents 49d6b40 + 61678b1 commit 88bf6d3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+2725
-181
lines changed

docs/agents.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ print(result.output)
5757
4. `result.output` will be a boolean indicating if the square is a winner. Pydantic performs the output validation, and it'll be typed as a `bool` since its type is derived from the `output_type` generic parameter of the agent.
5858

5959
!!! tip "Agents are designed for reuse, like FastAPI Apps"
60-
Agents are intended to be instantiated once (frequently as module globals) and reused throughout your application, similar to a small [FastAPI][fastapi.FastAPI] app or an [APIRouter][fastapi.APIRouter].
60+
You can instantiate one agent and use it globally throughout your application, as you would a small [FastAPI][fastapi.FastAPI] app or an [APIRouter][fastapi.APIRouter], or dynamically create as many agents as you want. Both are valid and supported ways to use agents.
6161

6262
## Running Agents
6363

@@ -708,7 +708,7 @@ print(result_sync.output)
708708
The final request uses `temperature=0.0` (run-time), `max_tokens=500` (from model), demonstrating how settings merge with run-time taking precedence.
709709

710710
!!! note "Model Settings Support"
711-
Model-level settings are supported by all concrete model implementations (OpenAI, Anthropic, Google, etc.). Wrapper models like `FallbackModel`, `WrapperModel`, and `InstrumentedModel` don't have their own settings - they use the settings of their underlying models.
711+
Model-level settings are supported by all concrete model implementations (OpenAI, Anthropic, Google, etc.). Wrapper models like [`FallbackModel`](models/overview.md#fallback-model), [`WrapperModel`][pydantic_ai.models.wrapper.WrapperModel], and [`InstrumentedModel`][pydantic_ai.models.instrumented.InstrumentedModel] don't have their own settings - they use the settings of their underlying models.
712712

713713
### Model specific settings
714714

docs/builtin-tools.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,47 @@ These tools are passed to the agent via the `builtin_tools` parameter and are ex
2020

2121
If a provider supports a built-in tool that is not currently supported by Pydantic AI, please file an issue.
2222

23+
## Dynamic Configuration
24+
25+
Sometimes you need to configure a built-in tool dynamically based on the [run context][pydantic_ai.tools.RunContext] (e.g., user dependencies), or conditionally omit it. You can achieve this by passing a function to `builtin_tools` that takes [`RunContext`][pydantic_ai.tools.RunContext] as an argument and returns an [`AbstractBuiltinTool`][pydantic_ai.builtin_tools.AbstractBuiltinTool] or `None`.
26+
27+
This is particularly useful for tools like [`WebSearchTool`][pydantic_ai.builtin_tools.WebSearchTool] where you might want to set the user's location based on the current request, or disable the tool if the user provides no location.
28+
29+
```python {title="dynamic_builtin_tool.py"}
30+
from pydantic_ai import Agent, RunContext, WebSearchTool
31+
32+
33+
async def prepared_web_search(ctx: RunContext[dict]) -> WebSearchTool | None:
34+
if not ctx.deps.get('location'):
35+
return None
36+
37+
return WebSearchTool(
38+
user_location={'city': ctx.deps['location']},
39+
)
40+
41+
agent = Agent(
42+
'openai-responses:gpt-5',
43+
builtin_tools=[prepared_web_search],
44+
deps_type=dict,
45+
)
46+
47+
# Run with location
48+
result = agent.run_sync(
49+
'What is the weather like?',
50+
deps={'location': 'London'},
51+
)
52+
print(result.output)
53+
#> It's currently raining in London.
54+
55+
# Run without location (tool will be omitted)
56+
result = agent.run_sync(
57+
'What is the capital of France?',
58+
deps={'location': None},
59+
)
60+
print(result.output)
61+
#> The capital of France is Paris.
62+
```
63+
2364
## Web Search Tool
2465

2566
The [`WebSearchTool`][pydantic_ai.builtin_tools.WebSearchTool] allows your agent to search the web,

docs/models/anthropic.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ Anthropic supports [prompt caching](https://docs.anthropic.com/en/docs/build-wit
8787
3. **Cache Tool Definitions**: Set [`AnthropicModelSettings.anthropic_cache_tool_definitions`][pydantic_ai.models.anthropic.AnthropicModelSettings.anthropic_cache_tool_definitions] to `True` (uses 5m TTL by default) or specify `'5m'` / `'1h'` directly
8888
4. **Cache All Messages**: Set [`AnthropicModelSettings.anthropic_cache_messages`][pydantic_ai.models.anthropic.AnthropicModelSettings.anthropic_cache_messages] to `True` to automatically cache all messages
8989

90+
!!! note "Amazon Bedrock"
91+
When using `AsyncAnthropicBedrock`, the TTL parameter is automatically omitted from all cache control settings (including `CachePoint`, `anthropic_cache_instructions`, `anthropic_cache_tool_definitions`, and `anthropic_cache_messages`) because Bedrock doesn't support explicit TTL.
92+
9093
### Example 1: Automatic Message Caching
9194

9295
Use `anthropic_cache_messages` to automatically cache all messages up to and including the newest user message:

docs/models/overview.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,7 @@ in sequence until one successfully returns a result. Under the hood, Pydantic AI
8787
from one model to the next if the current model returns a 4xx or 5xx status code.
8888

8989
!!! note
90-
91-
The provider SDKs on which Models are based (like OpenAI, Anthropic, etc.) often have built-in retry logic that can delay the `FallbackModel` from activating.
90+
The provider SDKs on which Models are based (like OpenAI, Anthropic, etc.) often have built-in retry logic that can delay the `FallbackModel` from activating.
9291

9392
When using `FallbackModel`, it's recommended to disable provider SDK retries to ensure immediate fallback, for example by setting `max_retries=0` on a [custom OpenAI client](openai.md#custom-openai-client).
9493

@@ -173,7 +172,9 @@ In the year 2157, Captain Maya Chen piloted her spacecraft through the vast expa
173172

174173
In this example, if the OpenAI model fails, the agent will automatically fall back to the Anthropic model with its own configured settings. The `FallbackModel` itself doesn't have settings - it uses the individual settings of whichever model successfully handles the request.
175174

176-
In this next example, we demonstrate the exception-handling capabilities of `FallbackModel`.
175+
### Exception Handling
176+
177+
The next example demonstrates the exception-handling capabilities of `FallbackModel`.
177178
If all models fail, a [`FallbackExceptionGroup`][pydantic_ai.exceptions.FallbackExceptionGroup] is raised, which
178179
contains all the exceptions encountered during the `run` execution.
179180

@@ -230,3 +231,6 @@ By default, the `FallbackModel` only moves on to the next model if the current m
230231
[`ModelAPIError`][pydantic_ai.exceptions.ModelAPIError], which includes
231232
[`ModelHTTPError`][pydantic_ai.exceptions.ModelHTTPError]. You can customize this behavior by
232233
passing a custom `fallback_on` argument to the `FallbackModel` constructor.
234+
235+
!!! note
236+
Validation errors (from [structured output](../output.md#structured-output) or [tool parameters](../tools.md)) do **not** trigger fallback. These errors use the [retry mechanism](../agents.md#reflection-and-self-correction) instead, which re-prompts the same model to try again. This is intentional: validation errors stem from the non-deterministic nature of LLMs and may succeed on retry, whereas API errors (4xx/5xx) generally indicate issues that won't resolve by retrying the same request.

docs/thinking.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ See the sections below for how to enable thinking for each provider.
1111
When using the [`OpenAIChatModel`][pydantic_ai.models.openai.OpenAIChatModel], text output inside `<think>` tags are converted to [`ThinkingPart`][pydantic_ai.messages.ThinkingPart] objects.
1212
You can customize the tags using the [`thinking_tags`][pydantic_ai.profiles.ModelProfile.thinking_tags] field on the [model profile](models/openai.md#model-profile).
1313

14+
Some [OpenAI-compatible model providers](models/openai.md#openai-compatible-models) might also support native thinking parts that are not delimited by tags. Instead, they are sent and received as separate, custom fields in the API. Typically, if you are calling the model via the `<provider>:<model>` shorthand, Pydantic AI handles it for you. Nonetheless, you can still configure the fields with [`openai_chat_thinking_field`][pydantic_ai.profiles.openai.OpenAIModelProfile.openai_chat_thinking_field].
15+
16+
If your provider recommends to send back these custom fields not changed, for caching or interleaved thinking benefits, you can also achieve this with [`openai_chat_send_back_thinking_parts`][pydantic_ai.profiles.openai.OpenAIModelProfile.openai_chat_send_back_thinking_parts].
17+
1418
### OpenAI Responses
1519

1620
The [`OpenAIResponsesModel`][pydantic_ai.models.openai.OpenAIResponsesModel] can generate native thinking parts.

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ markdown_extensions:
288288
- pymdownx.tasklist:
289289
custom_checkbox: true
290290
- sane_lists # this means you can start a list from any number
291+
- pydantic_docs.mdext
291292

292293
watch:
293294
- pydantic_ai_slim

pydantic_ai_slim/pydantic_ai/_agent_graph.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from .output import OutputDataT, OutputSpec
3131
from .settings import ModelSettings
3232
from .tools import (
33+
BuiltinToolFunc,
3334
DeferredToolCallResult,
3435
DeferredToolResult,
3536
DeferredToolResults,
@@ -148,7 +149,7 @@ class GraphAgentDeps(Generic[DepsT, OutputDataT]):
148149

149150
history_processors: Sequence[HistoryProcessor[DepsT]]
150151

151-
builtin_tools: list[AbstractBuiltinTool] = dataclasses.field(repr=False)
152+
builtin_tools: list[AbstractBuiltinTool | BuiltinToolFunc[DepsT]] = dataclasses.field(repr=False)
152153
tool_manager: ToolManager[DepsT]
153154

154155
tracer: Tracer
@@ -395,9 +396,23 @@ async def _prepare_request_parameters(
395396
else:
396397
function_tools.append(tool_def)
397398

399+
# resolve dynamic builtin tools
400+
builtin_tools: list[AbstractBuiltinTool] = []
401+
if ctx.deps.builtin_tools:
402+
run_context = build_run_context(ctx)
403+
for tool in ctx.deps.builtin_tools:
404+
if isinstance(tool, AbstractBuiltinTool):
405+
builtin_tools.append(tool)
406+
else:
407+
t = tool(run_context)
408+
if inspect.isawaitable(t):
409+
t = await t
410+
if t is not None:
411+
builtin_tools.append(t)
412+
398413
return models.ModelRequestParameters(
399414
function_tools=function_tools,
400-
builtin_tools=ctx.deps.builtin_tools,
415+
builtin_tools=builtin_tools,
401416
output_mode=output_schema.mode,
402417
output_tools=output_tools,
403418
output_object=output_schema.object_def,

pydantic_ai_slim/pydantic_ai/agent/__init__.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from ..settings import ModelSettings, merge_model_settings
4444
from ..tools import (
4545
AgentDepsT,
46+
BuiltinToolFunc,
4647
DeferredToolResults,
4748
DocstringFormat,
4849
GenerateToolJsonSchema,
@@ -171,7 +172,7 @@ def __init__(
171172
validation_context: Any | Callable[[RunContext[AgentDepsT]], Any] = None,
172173
output_retries: int | None = None,
173174
tools: Sequence[Tool[AgentDepsT] | ToolFuncEither[AgentDepsT, ...]] = (),
174-
builtin_tools: Sequence[AbstractBuiltinTool] = (),
175+
builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] = (),
175176
prepare_tools: ToolsPrepareFunc[AgentDepsT] | None = None,
176177
prepare_output_tools: ToolsPrepareFunc[AgentDepsT] | None = None,
177178
toolsets: Sequence[AbstractToolset[AgentDepsT] | ToolsetFunc[AgentDepsT]] | None = None,
@@ -199,7 +200,7 @@ def __init__(
199200
validation_context: Any | Callable[[RunContext[AgentDepsT]], Any] = None,
200201
output_retries: int | None = None,
201202
tools: Sequence[Tool[AgentDepsT] | ToolFuncEither[AgentDepsT, ...]] = (),
202-
builtin_tools: Sequence[AbstractBuiltinTool] = (),
203+
builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] = (),
203204
prepare_tools: ToolsPrepareFunc[AgentDepsT] | None = None,
204205
prepare_output_tools: ToolsPrepareFunc[AgentDepsT] | None = None,
205206
mcp_servers: Sequence[MCPServer] = (),
@@ -225,7 +226,7 @@ def __init__(
225226
validation_context: Any | Callable[[RunContext[AgentDepsT]], Any] = None,
226227
output_retries: int | None = None,
227228
tools: Sequence[Tool[AgentDepsT] | ToolFuncEither[AgentDepsT, ...]] = (),
228-
builtin_tools: Sequence[AbstractBuiltinTool] = (),
229+
builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] = (),
229230
prepare_tools: ToolsPrepareFunc[AgentDepsT] | None = None,
230231
prepare_output_tools: ToolsPrepareFunc[AgentDepsT] | None = None,
231232
toolsets: Sequence[AbstractToolset[AgentDepsT] | ToolsetFunc[AgentDepsT]] | None = None,
@@ -438,7 +439,7 @@ def iter(
438439
usage: _usage.RunUsage | None = None,
439440
infer_name: bool = True,
440441
toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None,
441-
builtin_tools: Sequence[AbstractBuiltinTool] | None = None,
442+
builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None,
442443
) -> AbstractAsyncContextManager[AgentRun[AgentDepsT, OutputDataT]]: ...
443444

444445
@overload
@@ -457,7 +458,7 @@ def iter(
457458
usage: _usage.RunUsage | None = None,
458459
infer_name: bool = True,
459460
toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None,
460-
builtin_tools: Sequence[AbstractBuiltinTool] | None = None,
461+
builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None,
461462
) -> AbstractAsyncContextManager[AgentRun[AgentDepsT, RunOutputDataT]]: ...
462463

463464
@asynccontextmanager
@@ -476,7 +477,7 @@ async def iter(
476477
usage: _usage.RunUsage | None = None,
477478
infer_name: bool = True,
478479
toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None,
479-
builtin_tools: Sequence[AbstractBuiltinTool] | None = None,
480+
builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None,
480481
) -> AsyncIterator[AgentRun[AgentDepsT, Any]]:
481482
"""A contextmanager which can be used to iterate over the agent graph's nodes as they are executed.
482483

0 commit comments

Comments
 (0)