Skip to content

Conversation

rmaceissoft
Copy link
Contributor

@rmaceissoft rmaceissoft commented Apr 14, 2025

Purpose

Allow tools to be customized and switched off on each step from one place
Motivated by this comment

Technical Details:

The prepare_tools param is an optional callable with the following signature:

async def prepare_tools(
  ctx: RunContext[DepsT],
  tools: list[ToolDefinition]
) -> list[ToolDefinition] | None

This param provides a hook for customizing or filtering available tools just before they are sent to the model and it:

  • Receives two parameters:
    • The current run context.
    • A list of tool definitions (combined from both agent tools and MCP server tools).
  • Is called after all tool definitions have been collected from:
    • Agent tools (via Tool.prepare_tool_def).
    • MCP servers registered into the Agent (via MCPServer.list_tools).
  • Only receives tool definitions that were successfully prepared:
    • Tools where Tool.prepare_tool_def returned None are excluded.
    • Tools that are not currently active on the MCP server are excluded.
  • Must return a list of tool definitions to be used for the current model request, or None if no tools are to be used.
  • Can modify and / or filter any tool definitions as needed.

Use Cases

  1. Dynamic Tool Registration (filtering out tools when certain contextual conditions are met)

    async def prepare_tools(ctx: RunContext[bool], tool_defs: list[ToolDefinition]) -> list[ToolDefinition] | None:
        # Enable certain tools depending on the run context conditions.
        if ctx.deps:
            return [tool_def for tool_def in tool_defs if tool_def.name == "search"]
        return tool_defs
  2. Custom Tool Definitions (force strict mode when using openai models)

    async def prepare_tools(ctx: RunContext[None], tool_defs: list[ToolDefinition]) -> list[ToolDefinition] | None:
        # Modify tool definitions based on context (if it's an OpenAI model)
        if ctx.model.system == "openai":
            return [replace(tool_def, strict=True) for tool_def in tool_defs]
        return tool_defs

TODO

  • Add tests
  • Update documentation

@rmaceissoft rmaceissoft marked this pull request as draft April 14, 2025 17:42
@rmaceissoft rmaceissoft marked this pull request as ready for review April 19, 2025 02:18
@DouweM
Copy link
Collaborator

DouweM commented Apr 21, 2025

@rmaceissoft Thank you! Having an agent-wide prepare_tools function will be super useful, but instead of passing it as an argument, what do you think of defining a new @agent.prepare_tools decorator that can be applied to a function?

That'd be less consistent with the Tool prepare arg, but feel more similar to other agent-wide functions like @agent.tool_plain, @agent.system_prompt, @agent.output_validator etc. One possibly inconsistency would be that there can be multiple of those, but (as implemented) only one prepare_tools, so subsequent decorator calls would override previous ones. Unless we support multiple, and run through them order. That could be useful if one function filters tools, and another modifies the schemas, for example.

@dmontagu What do you think?

@rmaceissoft
Copy link
Contributor Author

rmaceissoft commented Apr 23, 2025

@rmaceissoft Thank you! Having an agent-wide prepare_tools function will be super useful, but instead of passing it as an argument, what do you think of defining a new @agent.prepare_tools decorator that can be applied to a function?

That'd be less consistent with the Tool prepare arg, but feel more similar to other agent-wide functions like @agent.tool_plain, @agent.system_prompt, @agent.output_validator etc. One possibly inconsistency would be that there can be multiple of those, but (as implemented) only one prepare_tools, so subsequent decorator calls would override previous ones. Unless we support multiple, and run through them order. That could be useful if one function filters tools, and another modifies the schemas, for example.

@dmontagu What do you think?

@DouweM thanks for the suggestion, I really like the idea! Defining @agent.prepare_tools as a decorator feels more Pythonic and aligns nicely with other agent-wide functions. It also improves discoverability and readability in the long run.

I'm also on board with supporting multiple prepare_tools functions and chaining them in order — that could be quite powerful for composing tool transformations as mentioned

Happy to explore and implement that change if we align on the direction!

We could also consider supporting both styles — passing it as an argument and using the decorator — similar to how tools work. That would add some nice flexibility without too much overhead.

@DouweM DouweM self-assigned this Apr 25, 2025
@DouweM
Copy link
Collaborator

DouweM commented Apr 30, 2025

@rmaceissoft Before giving you the go-ahead: @Kludex Do you think this is reasonable?

This mechanism could also help us clean up some of the tool prep we're already doing, like in openai._customize_request_parameters.

@DouweM DouweM marked this pull request as draft April 30, 2025 19:06
@DouweM
Copy link
Collaborator

DouweM commented Apr 30, 2025

Something similar will likely be implemented for MCP tools, on a per-server level: #1220 (comment).

The most consistent route between these two paths may be a prepare_tools function passed to the Agent or MCPServer, so pretty much what you implemented initially.

@rmaceissoft
Copy link
Contributor Author

Hi team! Just leaving an update here:

I've merged the latest changes from main to resolve conflicts and ensure CI is running properly.

Regarding the idea of passing a prepare_tools function to the MCPServer, I see the rationale, especially if multiple agents share the same server. That said, the current implementation (attaching it to the Agent) also supports that use case and additionally allows filtering based on the RunContext, which wouldn’t be possible at the MCP server level (since MCPServer.list_tools doesn’t have access to RunContext).

As for the comment about transforming request parameters per model, I understand the motivation to streamline that logic, but I believe it may be better handled in a separate PR, as it addresses a different concern.

Let me know how you'd like to proceed. Happy to revise the PR as needed, just waiting for feedback from the team before making further changes.

Copy link
Contributor

hyperlint-ai bot commented May 12, 2025

PR Change Summary

Added a new parameter prepare_tools to the Agent class, enabling dynamic customization of tool definitions based on the run context.

  • Introduced the prepare_tools parameter for the Agent class to allow dynamic tool customization.
  • Updated documentation to include examples of using prepare_tools for filtering and modifying tool definitions.
  • Provided use cases demonstrating the functionality of the new parameter.

Modified Files

  • docs/tools.md

How can I customize these reviews?

Check out the Hyperlint AI Reviewer docs for more information on how to customize the review.

If you just want to ignore it on this PR, you can add the hyperlint-ignore label to the PR. Future changes won't trigger a Hyperlint review.

Note specifically for link checks, we only check the first 30 links in a file and we cache the results for several hours (for instance, if you just added a page, you might experience this). Our recommendation is to add hyperlint-ignore to the PR to ignore the link check for this PR.

- Make `ToolsPrepareFunc` available directly from the `pydantic_ai.tools` module.
- Enhance documentation for the agent-wide `prepare_tools` feature.
- Fix return type hints in usage examples for clarity and correctness.
@rmaceissoft rmaceissoft requested a review from DouweM May 12, 2025 20:26
@DouweM DouweM requested a review from Kludex May 13, 2025 10:48
@DouweM DouweM removed their assignment May 13, 2025
@Kludex Kludex marked this pull request as ready for review May 14, 2025 09:08
Copy link
Member

@Kludex Kludex left a comment

Choose a reason for hiding this comment

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

The API looks good @DouweM 👍

All my comments are minor, but I'd like them to be addressed. 👀

@rmaceissoft rmaceissoft requested a review from Kludex May 14, 2025 18:51
Co-authored-by: Reiner Marquez <[email protected]>
@DouweM DouweM enabled auto-merge (squash) May 20, 2025 15:02
@DouweM DouweM merged commit d896b01 into pydantic:main May 20, 2025
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants