Skip to content

Conversation

@varunursekar
Copy link

@varunursekar varunursekar commented Dec 4, 2025

When @function_tool is used, no reference to the original function is retained, which makes testing the function directly cumbersome. The original function is now just stored as an attribute of FunctionTool:

@function_tool
def my_func(x: str, y: str) -> str:
  return x + y

# works like the original function
my_func.func('hello ', 'world!')

The alternative is to require users to define helper functions duplicating signatures and doc strings:

# using double definitions instead
def _my_func(x: str, y: str) -> str:
  return x + y

@function_tool
def my_func(x: str, y: str) -> str:
  return _my_func(x, y)

Copy link
Member

@seratch seratch left a comment

Choose a reason for hiding this comment

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

Thanks for sending this pull request. Ideally, enabling my_func('hello ', 'world!') is the best. I will also explore solutions for it. As for your changes, make lint seems to be failing. Can you resolve it?

@seratch seratch added this to the 0.6.x milestone Dec 4, 2025
@varunursekar
Copy link
Author

@seratch Thanks for the quick response.

To achieve the functionality you are describing we could adopt the following approach:

import inspect
from dataclasses import dataclass
import functools


@dataclass
class FunctionTool:
    ....
    func: ToolFunction[...] | None = None

    def __post_init__(self):
        if self.func:
          functools.update_wrapper(self, self.func)

    def __call__(self, *args, **kwargs):
        if not self.func:
          raise FunctionNotSetError()
        return self.func(*args, **kwargs)

def func(x: str) -> str:
  """this is a test"""
    return x

func = FunctionTool(...,func=func)

sig = inspect.signature(func)
sig_func = inspect.signature(func.func)

assert sig == sig_func
assert func.__doc__ == func.func.__doc__

This will ensure that signatures and docs of the original function are retained. What do you think?

@seratch
Copy link
Member

seratch commented Dec 4, 2025

Yeah you're right. I actually did the same for Slack's official SDKs before joining OpenAI :)

If you could apply the approach rather than holding func object in FunctionTool instance, it should be better for dev experience. I don't think this approach could cause any side effects but we can verify if nothing is broken due to that.

@varunursekar
Copy link
Author

@seratch I implemented the solution I proposed. A few notes:

  1. I explored the idea of passing in func as an init-only variable that is not stored as an attribute of the class. One way would be to override the __init__ method of the dataclass - but I decided against this because it requires copying all arguments and their defaults in order, which seemed not great for maintainability. Another option would be to use dataclass InitVars. However, storing the function as a private attribute _func seemed simpler to me.
  2. While the __call__ method of FunctionTool is sync, it technically also handles the async case since it will simply return a coroutine, which the user can await.
  3. Caveat to 2: The downside is that the user doesn't have a way of inspecting that the FunctionTool returns a coroutine, since inspect.iscoroutinefunction doesn't return True for async callable classes (only async functions) and FunctionTool.__call__ is sync. One way around this would be to create a subclass AsyncFunctionTool that has an async __call__ method instead. However, I don't think this should be too much of an issue since the behaviour should be apparent from the original function definition itself.

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.

2 participants