Skip to content

Don't follow wrappers when inspecting signatures #2787

@tseaver

Description

@tseaver

Initial Checks

Description

I am building up tools for my agents from static configuration. Because I'd like to be able to use my tool functions without the agent context, I want to "curry" the configuration into the tool functions passed to the agent. E.g., the script below configures a tool function with two separate tool configurations to create two tools for the agent.

That script only works if I apply the following patch to pydantic_ai._function_schema.function_schema:

--- a/_function_schema.py
+++ b/_function_schema.py
@@ -99,7 +99,7 @@ def function_schema(  # noqa: C901
     errors: list[str] = []
 
     try:
-        sig = signature(function)
+        sig = signature(function, follow_wrapped=False)
     except ValueError as e:
         errors.append(str(e))
         sig = signature(lambda: None)
:

A similar patch to pydantic_ai._function_schema._is_ctx would also make it regular:

--- a/_function_schema.py
+++ b/_function_schema.py
@@ -244,7 +244,7 @@ def _takes_ctx(function: TargetFunc[P, R]) -> TypeIs[WithCtx[P, R]]:
         `True` if the function takes a `RunContext` as first argument, `False` otherwise.
     """
     try:
-        sig = signature(function)
+        sig = signature(function, follow_wrapped=False)
     except ValueError:  # pragma: no cover
         return False  # pragma: no cover
     try:

I believe that I should be able to pass through a functools.partial instance as a tool function, as long as I fix up any values which are needed.

Example Code

import functools
import importlib
import inspect
import math

import pydantic_ai

SYSTEM_PROMPT = """\
This is a test
"""


PI_MULTIPLES_CONFIG = {
    "name": "pi_multiples",
    "description": "Return multiples of math.pi",
    "start_value": "math.pi",
}


E_MULTIPLES_CONFIG = {
    "name": "e_multiples",
    "description": "Return multiples of math.e",
    "start_value": "math.e",
}


def multiples(n_multiples: int, tool_config: dict) -> list[float]:
    """Multiply a given value by a range of multiples

    Params:
        n_multiples:  the number of multiples to return
    """
    module_name, tool_name = tool_config["start_value"].rsplit(".", 1)
    module = importlib.import_module(module_name)
    start_value = getattr(module, tool_name)

    print(f"Multiplying {start_value} by {n_multiples} multiples")

    return [start_value * i for i in range(n_multiples)]


def agent_tool(tool_func, tool_config: dict):

    curried_tool_func = functools.update_wrapper(
        functools.partial(tool_func, tool_config=tool_config),
        tool_func,
    )
    tool = pydantic_ai.Tool(
        curried_tool_func,
        takes_ctx=False,
        name=tool_config["name"],
        description=tool_config["description"],
    )
    json_schema = tool.function_schema.json_schema
    del json_schema["properties"]["tool_config"]
    return tool


def main():

    configured_tool_specs = [
        (multiples, PI_MULTIPLES_CONFIG),
        (multiples, E_MULTIPLES_CONFIG),
    ]

    configured_tools = [
        agent_tool(tool_func, tool_config)
        for (tool_func, tool_config) in configured_tool_specs
    ]

    agent = pydantic_ai.Agent(
        model='openai:gpt-4o',
        instructions=SYSTEM_PROMPT,
        tools=configured_tools,
    )

    result = agent.run_sync("Multiply pi times three?")
    print(result.output)

    result = agent.run_sync("Multiply e times four?")

if __name__ == "__main__":
    main()

Python, Pydantic AI & LLM client version

Python 3.13.5
pydantic_ai 0.7.0
LLM client None

Metadata

Metadata

Assignees

Labels

questionFurther information is requested

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions