langchain-ai / langchain

🦜🔗 Build context-aware reasoning applications
https://python.langchain.com
MIT License
94.89k stars 15.37k forks source link

AttributeError: 'CustomTool' object has no attribute 'fields_set' #12304

Closed usersina closed 1 year ago

usersina commented 1 year ago

System Info

python=3.11 langchain=0.0.314 pydantic=2.3.0 pydantic_core=2.6.3

Who can help?

No response

Information

Related Components

Reproduction

From the documentation, I can create a fake tool that works as follows:

# Define the tool
class FakeSearchTool(BaseTool):
    name: str | None = "custom_search"
    description: str | None = "useful for when you need to answer questions about current events"

    def _run(
        self, query: str
    ) -> str:
        """Use the tool."""
        print("Query:", query)
        return "Used the tool, and got the answer!"

    async def _arun(
        self, query: str
    ) -> str:
        """Use the tool asynchronously."""
        raise NotImplementedError("custom_search does not support async")

# Test if the tool is working
search_tool = FakeSearchTool()
print(search_tool("What is my answer?"))

Query: What is my answer? Used the tool, and got the answer!

My case however requires me to give an initial configuration object to every tool, something like this:

from langchain.tools.base import BaseTool

# Create the config class
class Config:
    def __init__(self, init_arg1: str, init_arg2: str):
        self.init_arg1 = init_arg1
        self.init_arg2 = init_arg2

# Create the tool
class FakeSearchTool(BaseTool):
    name: str | None = "custom_search"
    description: str | None = "useful for when you need to answer questions about current events"
    config: Config

    def __init__(self, config: Config):
        self.config = config

    def _run(
        self, query: str
    ) -> str:
        """Use the tool."""
        print("Query:", query)
        print("Obtained config:", self.config.init_arg1, self.config.init_arg2)
        return "Used the tool, and got the answer using the config!"

    async def _arun(
        self, query: str
    ) -> str:
        """Use the tool asynchronously."""
        raise NotImplementedError("custom_search does not support async")

# Test if the tool is working
search_tool = FakeSearchTool(
    config=Config(init_arg1="arg1", init_arg2="arg2")
)
print(search_tool("What is my answer?"))

However, this does not work and return the following error:

AttributeError: 'FakeSearchTool' object has no attribute '__fields_set__'

Stack Trace ``` --------------------------------------------------------------------------- AttributeError Traceback (most recent call last) /ai-service/notebooks/agent-with-elasticsearch-tool.ipynb Cell 18 line 2 20 """Use the tool asynchronously.""" 21 raise NotImplementedError("custom_search does not support async") ---> 23 search_tool = FakeSearchTool( 24 config=Config(init_arg1="arg1", init_arg2="arg2") 25 ) 26 print(search_tool("What is my answer?")) /ai-service/notebooks/agent-with-elasticsearch-tool.ipynb Cell 18 line 7 6 def __init__(self, config: Config): ----> 7 self.config = config File ~/.pyenv/versions/3.11.5/envs/ai-service/lib/python3.11/site-packages/pydantic/v1/main.py:405, in BaseModel.__setattr__(self, name, value) 402 else: 403 self.__dict__[name] = value --> 405 self.__fields_set__.add(name) AttributeError: 'FakeSearchTool' object has no attribute '__fields_set__' ```

Expected behavior

I expect to get the following output out of the tool:

Query: What is my answer? Obtained config: arg1 arg2 Used the tool, and got the answer using the config!

dosubot[bot] commented 1 year ago

🤖

Hi @usersina, good to see you again!

The error you're encountering is due to the way you're trying to initialize your custom tool. In LangChain, the BaseTool class uses Pydantic for data validation and parsing. When you try to assign a value to the config attribute in your __init__ method, Pydantic attempts to access the __fields_set__ attribute, which is not present because your FakeSearchTool class is not a Pydantic model.

To resolve this issue, you can make your config attribute a Pydantic model. Here's an example of how you can do this:

from pydantic import BaseModel

class Config(BaseModel):
    init_arg1: str
    init_arg2: str

class FakeSearchTool(BaseTool):
    name: str | None = "custom_search"
    description: str | None = "useful for when you need to answer questions about current events"
    config: Config

    def __init__(self, config: Config):
        self.config = config

    def _run(
        self, query: str
    ) -> str:
        """Use the tool."""
        print("Query:", query)
        print("Obtained config:", self.config.init_arg1, self.config.init_arg2)
        return "Used the tool, and got the answer using the config!"

    async def _arun(
        self, query: str
    ) -> str:
        """Use the tool asynchronously."""
        raise NotImplementedError("custom_search does not support async")

# Test if the tool is working
search_tool = FakeSearchTool(
    config=Config(init_arg1="arg1", init_arg2="arg2")
)
print(search_tool("What is my answer?"))

This should resolve the AttributeError you're seeing. If you encounter any other issues, feel free to ask. Happy coding!

Sources


This response is meant to be useful and save you time. It is not meant to be a precise solution, but rather a starting point for your own research.

Help me be more useful! Please leave a 👍 if this is helpful and 👎 if it is irrelevant.

If you want to continue the conversation, start your reply with @dosu-bot.

eyurtsev commented 1 year ago

@usersina which version of pydantic are you using? If you're using v2 follow guidelines here: https://python.langchain.com/docs/guides/pydantic_compatibility

usersina commented 1 year ago

I updated my description to include the installed pydantic version and the created Config class (which I previously forgot) hence completing the minimal reproducible example.

@eyurtsev according to the link you sent langchain>=0.0.267 internally uses pydantic v1. I don't use pydantic imports anywhere in the example above so I don't have a version problem if I understand correctly.

That said, I tried the @dosubot's suggestion with a slight tweaking, by importing from pydantic.v1 instead, but I still see the same error:

from langchain.tools.base import BaseTool
from pydantic.v1 import BaseModel

# Create the config class
class Config(BaseModel):
    init_arg1: str = "default value"
    init_arg1: str = "default value 2"

    def __init__(self, init_arg1: str, init_arg2: str):
        self.init_arg1 = init_arg1
        self.init_arg2 = init_arg2

# Create the tool
class FakeSearchTool(BaseTool):
    name: str | None = "custom_search"
    description: str | None = "useful for when you need to answer questions about current events"
    config: Config

    def __init__(self, config: Config):
        self.config = config

    def _run(
        self, query: str
    ) -> str:
        """Use the tool."""
        print("Query:", query)
        print("Obtained config:", self.config.init_arg1, self.config.init_arg2)
        return "Used the tool, and got the answer using the config!"

    async def _arun(
        self, query: str
    ) -> str:
        """Use the tool asynchronously."""
        raise NotImplementedError("custom_search does not support async")

# Test the tool
search_tool = FakeSearchTool(
    config=Config(init_arg1="arg1", init_arg2="arg2")
)
print(search_tool("What is my answer?"))

AttributeError: 'Config' object has no attribute '__fields_set__'

dosubot[bot] commented 1 year ago

🤖

The error you're encountering is likely due to the way you're initializing your Config class. In Pydantic, the __init__ method is not typically overridden. Instead, Pydantic uses the class attributes to automatically generate an __init__ method. When you override the __init__ method, Pydantic's auto-generated __init__ method is not called, which is likely why you're encountering the AttributeError: 'Config' object has no attribute '__fields_set__'.

To resolve this issue, you should remove the __init__ method from your Config class and let Pydantic handle the initialization. Here's how you can modify your Config class:

from pydantic import BaseModel

class Config(BaseModel):
    init_arg1: str = "default value"
    init_arg2: str = "default value 2"

With this modification, you can initialize a Config object like so:

config = Config(init_arg1="arg1", init_arg2="arg2")

Pydantic will automatically use the class attributes to generate an __init__ method that accepts the specified arguments.

Also, I noticed a typo in your Config class. You have init_arg1 defined twice. I assume the second one should be init_arg2.

I hope this helps! Let me know if you have any other questions.

Sources


This response is meant to be useful and save you time. It is not meant to be a precise solution, but rather a starting point for your own research.

Help me be more useful! Please leave a 👍 if this is helpful and 👎 if it is irrelevant.

If you want to continue the conversation, start your reply with @dosu-bot.

usersina commented 1 year ago

Oh, good bot! That actually pointed me to the right answer.

The config can be as is though, I leave it as follows:

class Config:
    def __init__(self, init_arg1: str, init_arg2: str):
        self.init_arg1 = init_arg1
        self.init_arg2 = init_arg2

But then I don't create any __init__ function for my custom tool (the crux of the matter)

class FakeSearchTool(BaseTool):
    name: str | None = "custom_search"
    description: str | None = "useful for when you need to answer questions about current events"
    config: Config

    def _run(
        self, query: str
    ) -> str:
        """Use the tool."""
        print("Query:", query)
        print("Obtained config:", self.config.init_arg1, self.config.init_arg2)
        return "Used the tool, and got the answer using the config!"

    async def _arun(
        self, query: str
    ) -> str:
        """Use the tool asynchronously."""
        raise NotImplementedError("custom_search does not support async")

And then this finally works!

search_tool = FakeSearchTool(
    config=Config(init_arg1="arg1", init_arg2="arg2")
)
print(search_tool("What is my answer?"))

Query: What is my answer? Obtained config: arg1 arg2 Used the tool, and got the answer using the config!

meet1919 commented 11 months ago

You can pass any custom variable to the tool with metadata argument.

Below is the BaseTool source code.

class BaseTool(RunnableSerializable[Union[str, Dict], Any]):
    metadata: Optional[Dict[str, Any]] = None
        """Optional metadata associated with the tool. Defaults to None
        This metadata will be associated with each call to this tool,
        and passed as arguments to the handlers defined in `callbacks`.
        You can use these to eg identify a specific instance of a tool with its use case.
        """

Below is example code on how to add metadata to the custom tool.

class CustomTool(BaseTool)  
    name = "jamesallen-search"
    description = "Use this tool as the primary source of context information. Always search for the answers using this tool first, don't make up answers yourself"
    return_direct = True
    args_schema: Type[BaseModel] = JamesAllenRetrivalSearchInput  

    def _run(self, question: str, run_manager: Optional[CallbackManagerForToolRun] = None) -> str:
        # External variable return when tool runs
        return self.metadata['custom_variable']

    async def _arun(self, question: str, run_manager: Optional[AsyncCallbackManagerForToolRun] = None) -> str:
        """Use the tool asynchronously."""
        raise NotImplementedError("Not implemented")

class Agent:

    def __init__(self):
        llm = ChatOpenAI(
            model_name='gpt-3.5-turbo-1106', 
            temperature=0.1,
        )
        memory = ConversationBufferMemory(output_key='output')
       self.custom_variable = True
        tools = [
            CustomTool(metadata={'custom_variable': self.custom_variable})
        ]
        self.agent = initialize_agent(
            tools=tools,
            llm=llm,
            agent="conversational-react-description",
            verbose=True,
            memory=memory,
            return_intermediate_steps=True,
        )