Skip to content
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

make sure that @tool decorator can be applied to a method (regression?) #27471

Open
1 task done
eyurtsev opened this issue Oct 18, 2024 · 16 comments · May be fixed by #28695
Open
1 task done

make sure that @tool decorator can be applied to a method (regression?) #27471

eyurtsev opened this issue Oct 18, 2024 · 16 comments · May be fixed by #28695
Assignees
Labels
Ɑ: core Related to langchain-core

Comments

@eyurtsev
Copy link
Collaborator

eyurtsev commented Oct 18, 2024

Privileged issue

  • I am a LangChain maintainer, or was asked directly by a LangChain maintainer to create an issue here.

Issue Content

from langchain_core.tools import tool

class MyAPI:
    def __init__(self):
        self.base_url = 'https://en.wikipedia.org/wiki/'

    @tool
    def lookup(self, page) -> str:
        """Look up a page on wikipedia. Include just the name of the page. Rest of the URL will be filled in."""
        url = self.base_url + item
        return request.get(url)        
        
api = MyAPI()
api.lookup.invoke('hello')
@eyurtsev eyurtsev self-assigned this Oct 18, 2024
@langcarl langcarl bot added the investigate Flagged for investigation. label Oct 18, 2024
@dosubot dosubot bot added the Ɑ: core Related to langchain-core label Oct 18, 2024
@GlassesNoGlasses
Copy link

Hey @eyurtsev! I'm part of a group of UofT developers interested in this issue. Can we tackle it? If so, what specific requirements and output are you looking for?

@eyurtsev
Copy link
Collaborator Author

Yes, you're welcome to try to tackle it. What kind of timeline do you have in mind?


We have a set of unit tests for tools: https://github.com/langchain-ai/langchain/blob/master/libs/core/tests/unit_tests/test_tools.py

We want to make sure that the @tool decorator has the correct behavior whether it's a standalone function or a method.

@tool
def foo(x: int, y: float) -> str:
  """bar"""
@tool
async def foo(x: int, y: float) -> str:
  """bar"""
class A:
  @tool
  def foo(self, x: int, y: float) -> str:
    """bar"""
class A:
  @tool
  async def foo(self, x: int, y: float) -> str:
    """bar"""

All behave the same.

  • The code should be executable with .invoke or .ainvoke
  • foo.args should yield the same schema. And the schema should not include self

@GlassesNoGlasses
Copy link

Thank you for the succinct requirements, that will really help us get started. Our timeline to get this finished is around 2 or 3 weeks from now, though the sooner the better. Does that work for you?

@eyurtsev
Copy link
Collaborator Author

Could you check in before getting started. This is in core so we may end up prioritizing this fix

@GlassesNoGlasses
Copy link

Hey @eyurtsev, the team and I are good to go! We are planning to finish the implementation and testing by November 8th and submit a PR.

@eyurtsev
Copy link
Collaborator Author

Sounds good. If you need feedback earlier than that you can send a PR in draft mode.

@eyurtsev eyurtsev removed the investigate Flagged for investigation. label Oct 24, 2024
@eyurtsev eyurtsev reopened this Oct 24, 2024
@GlassesNoGlasses
Copy link

Hey @eyurtsev,

Just wanted to follow up with the issue progress. We are finding difficulties in modifying the @tool decorator as described above in a way that allows for the same syntax you specified.

The main issue is the self parameter of a method and the lack of reference to the class/instance itself. Our original idea was to have some sort of wrapper for invoke that captures the value of self and will pass it in for you automatically. So .invoke({'arg': val}) turns into .invoke({'self': self, 'arg': val}). However, when decorating a method, you are actually just decorating a function that has a self parameter. This computation takes place before object initialization, and so there is no self object to capture.

That is why, as far as we have found, this:

class A:
    @tool
    def foo(self, x: int):
        """bar"""

a = A()
a.foo.invoke({'x': 5})

Is not able to be done. The tool decorator is not able to get any self value.

We have an alternative idea that we would be happy to implement which solves half of the original issue. This idea is to implement the invoke wrapper as described above, then users would be able to use it like this:

class A:
    def __init__(self):
        self.foo = tool(self.foo, selfRef=self) # tool decorator is modified to capture self value

    def foo(self, x: int):
        """bar"""

a = A()
a.foo.invoke({'x': 5})
print(a.foo.args) # { 'x': int } ...

# Inside of a.foo.invoke:

internal_tool.invoke({'self': self, 'x': 5})

By moving the computation into the init, the self value is able to be captured and such a wrapper can be properly implemented.

What do you think? Are we overlooking something here, and the original issue is able to be solved using the syntax you asked for? If not, do you think it is worth implementing this?

@GlassesNoGlasses
Copy link

Hey @eyurtsev, just wanted to follow up on the above comment.

@efriis
Copy link
Member

efriis commented Nov 13, 2024

Would this example help? This should be achievable!

def my_decorator(f):
    def wrapper(self, a, b):
        return self.c + f(self, a, b)
    return wrapper

class A:
    def __init__(self, c):
        self.c = c

    @my_decorator
    def add(self, a, b):
        return a + b
    
a = A(10)
print(a.add(1, 2))  # 13

@ethanglide
Copy link

@efriis Thank you for your comment.

We have thought about using a wrapper in this way, but unfortunately it still won't work out in the way that Eugene originally asked for it to work.

The reason this is the case is because the tool decorator does not return a wrapper function but instead an object. The advantage to returning a wrapper function in your example is that when you call a.add(1, 2), the self reference to a also gets passed in as the self parameter since it is a method. However when the decorator creates an object, calling a.add.invoke({'a': 1, 'b': 2}) will not pass in any self reference to a which does not allow it to be used.

Here is an example of an idea that we had that was similar to what you suggested, but would not match the syntax that was originally asked for:

def my_decorator(func):
    def wrapper(self):
        return tool_with_self_reference(self, func) # the magic happens in here
    return wrapper

class A:
    def __init__(self, c):
        self.c = c

    @my_decorator
    def add(self, a, b):
        return a + b
    
a = A(10)
print(a.add().invoke({'a': 1, 'b': 2}))  # 13

In this case, the invoke will work as wanted and the a.add().args would work as wanted, but you have to call a.add() instead of a.add.

The solution we proposed in a previous comment moved that a.add() stuff into the constructor, so that in the actual code you can simple call a.add without needing the brackets. But both of them work in the same way. It's all about capturing the self reference so you can pass it into invoke automatically.

The question is whether or not this should be implemented, or if it is not worth it since it does not technically fix everything originally proposed.

@efriis
Copy link
Member

efriis commented Nov 13, 2024

something like this? python is very flexible, so I feel pretty confident it is possible.

def my_decorator(f):
    class Wrapper:
        def __init__(self, outer_self):
            self.outer_self = outer_self

        def invoke(self, a, b):
            return self.outer_self.c + f(self.outer_self, a, b)
            
    @property
    def wrapper(self):
        return Wrapper(self) # pass outer_self=self

    return wrapper


class A:
    def __init__(self, c):
        self.c = c

    @my_decorator
    def add(self, a, b):
        return a + b


a = A(10)
print(a.add.invoke(1, 2))  # 13

@ethanglide
Copy link

Wow @efriis I was not aware of this magic? I think this is the missing piece we needed. I'm going to try to see if we can throw something together with this. Thank you!

@ethanglide
Copy link

Hello @efriis I have created a draft PR with the implementation for this. Thank you very much for your help.
For now I am having trouble with the linter, but I would really appreciate a review and a look at my unit tests to make sure they are exhaustive.

@dskarbrevik
Copy link

Hey, any progress on this issue? I've been looking around for a way to do this but seems it's still not possible. I'm curious why this is not a more common desire for tools.

@ethanglide
Copy link

Hello @dskarbrevik, yes there is progress on this issue. There is a PR I’ve made implementing this feature which is currently in the review process.

#28160

@dskarbrevik
Copy link

Amazing, thanks for your work to make it happen! Hope it gets merged soon.

FWIW I tested your fork for my use case and it works :)

@ethanglide ethanglide linked a pull request Dec 12, 2024 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Ɑ: core Related to langchain-core
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants