-
Notifications
You must be signed in to change notification settings - Fork 15.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
core: method tools #28160
core: method tools #28160
Conversation
This is_method parameter will create a structured tool that automatically injects the self reference into the tool
More features for method tools: - automatic detection (no need to specify is_method=True) anymore, detects from the `self` parameter being present - support for multiple input types: strings, dictionaries, toolcalls
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
The CI checks have to do with me having trouble replacing the return types of the |
Hello @efriis just following up on this from the issue. I would appreciate it if any of the maintainers were able to give this a review. The functionality is ready, just the linter is upset about a few things. On that note, I am having trouble appeasing mypy and was wondering if I could get some help with making it happy here. Upon running
The first error comes from this new helper function in def _add_outer_self(self, input: Union[str, dict, ToolCall]) -> dict | ToolCall:
"""Add outer self into arguments for method tools."""
# If input is a string, then it is the first argument
if isinstance(input, str):
args = {"self": self.outer_self}
for x in self.args: # loop should only happen once
args[x] = input
return args
# ToolCall
if "type" in input and input["type"] == "tool_call":
input["args"]["self"] = self.outer_self
return input
# Dict
input["self"] = self.outer_self # <-- ERROR HERE
return input I am not able to use The second error comes from here inside the @property # <-- ERROR HERE
def method_tool(self: Callable) -> StructuredTool:
return StructuredTool.from_function(
func,
coroutine,
name=tool_name,
description=description,
return_direct=return_direct,
args_schema=schema,
infer_schema=infer_schema,
response_format=response_format,
parse_docstring=parse_docstring,
error_on_invalid_docstring=error_on_invalid_docstring,
outer_self=self,
) Where The third error is because the return types are changing. This one is being really difficult because every time I fix the return type on one function it will change the return type of the parent function it is nested within, and so on and so on. I am not an expert on the types in Python so cleaning this up has proven to be hard thing to do. I figured that someone with a bit more knowledge on the codebase would be able to clean up this refactor easily, so any potential help would be appreciated. That, or a way to get the same functionality without needing to change the return types somehow could save some time. |
Some documentation for this feature to help maintainers: When the When you call Once that |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how is this handled for classmethods, e.g. if you use A.foo
as a tool instead of a.foo
in the test?
If we could add a test case for that, that would be great!
sorry for the delay
Unfortunately, classmethod support will not work with 3.13 or higher. As classmethod is no longer able to wrap property methods. [source](https://docs.python.org/3.13/library/functions.html#classmethod)
Accidentally was skipping the wrong test, moved the decorator onto the correct test
@efriis I have added support and test cases for classmethods. Note that since we are using descriptors (the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks and appreciate your responsiveness for the back and forth!
@classmethod | ||
@tool | ||
def foo(cls, a: int, b: int) -> int: | ||
"""Add two numbers to c.""" | ||
return a + b + cls.c |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this case actually doesn't matter - I think the one that matters is this
@classmethod | |
@tool | |
def foo(cls, a: int, b: int) -> int: | |
"""Add two numbers to c.""" | |
return a + b + cls.c | |
@tool | |
@classmethod | |
def foo(cls, a: int, b: int) -> int: | |
"""Add two numbers to c.""" | |
return a + b + cls.c |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will be marking all the rest of the conversations surrounding this general issue as resolved. This thread here will be to discuss everything around the classmethod support.
Unfortunately, it seems that in order to get the desired behaviour the @classmethod
decorator must be applied last. This is because we want A.foo
to pass in the class A
as the cls
parameter. This behaviour does not seem to happen unless @classmethod
happens last.
Here is what happens with making the @tool
decorator return a method tool of the classmethod
s underlying function:
class A:
c = 5
@tool
@classmethod
def foo(cls, a: int, b: int) -> int:
"""Add two numbers to c."""
return a + b + cls.c
> assert A.foo.invoke({"a": 1, "b": 2}) == 8
E AttributeError: 'property' object has no attribute 'invoke'
It is no longer a classmethod
! The cls
value does not get passed in as we would wish for it to.
This can be fixed by making this simply apply the decorators in the desired order through some simple logic in convert.py
:
elif isinstance(name_or_callable, classmethod):
return classmethod(tool(name_or_callable.__func__))
However this is not everything that we want since Python 3.13 still removed wrapping descriptors (like property
s) with classmethod
. So it would still not be compatible with 3.13 and above. For this reason I would be able to support the syntax you've requested but would not be able to support newer Python versions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
from all these differences, what are your thoughts on introducing a new decorator tool_property
or something to distinguish these cases? Would we also need a tool_classmethod
to be separate too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some additional thoughts:
- in principle, it makes sense to force the user to distinguish rather than try to auto-detect that your thing is a property/classmethod
- especially for crazy edge cases like below, where the tool decorator would run in a non-class context, and then screw up later
@tool
def my_tool(...):
...
class A():
it = my_tool
A.it2 = my_tool
a = A()
a.it3 = my_tool
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having a separate decorator specifically for implementing this feature was always an idea, and is definitely the better solution.
To be honest, the reason things are implemented the way they currently are is because I thought that if I did not implement this in a way that satisfied the example code given by Eugene in the original issue then that issue would not be resolved.
Now that I know there is more leeway and freedom to implement this feature in the way that makes most sense rather than being locked into using the original tool decorator, that can absolutely be done.
How about I first create some sort of new decorator that is for method tools, and then from there I can see if classmethods are doable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sounds great!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doing it as 2 separate PRs is also good practice. If you'd like to avoid double-work, you're welcome to put up a PR with just the unit tests first, just to make sure we're envisioning the same interface
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check out my new PR #28695.
On top of doing what you suggested, I also came up with a much more elegant solution without any strange workarounds or shortcomings. The actual implementation is only something like 20 lines of added logic. Taking a second shot at this was definitely worth it.
closing in favor of the other one - it's a much better track agreed! #28695 |
Add support for methods to the
@tool
decorator. Previously using the@tool
decorator on methods was not possible. Now the@tool
decorator will have the same behaviours on methods as on regular functions.Resolves #27471
Example:
Note that no
self
argument had to be passed intoa.foo.invoke
. This would not work in the past even ifself
was passed in manually.