Skip to content

Class Tools

David-Andrew Samson edited this page May 1, 2023 · 13 revisions

Toolsets and Stateful Tools

It's straightforward to create sets of related tools, and stateful tools by using the provided @toolset decorator on a class. Simply:

  1. make a class containing all the functionality and methods you want the LLM to be able to access
  2. decorate the methods the LLM can use with @tool(). Undecorated methods will be hidden from the LLM
  3. decorate the whole class with @toolset()
  4. pass either the class constructor or an instance of the class with the list of tools given to the LLM agent
@toolset()
class MyToolset:
    """
    general explanation of the class and how it's used
    """
    def __init__(self, ...):
        ... #initialize, set member values, etc.
    
    @tool()
    def tool1(self, a:int) -> bool:
        """
        Simple description of what tool1 does

        More detailed description of the tool. This can be multiple lines.
        Explain what this tool does, how it's used, how it relates to other tools in the toolset. 
        Do not use python syntax in the description (since the LLM doesn't call functions that way).

        Args:
            a (int): Description of a

        Returns:
            bool: Description of the return value
        """
        ... #implementation

    @tool()
    def tool2(self, s:str, v:dict) -> float:
        """
        <full docstring for this method>
        """
        ... #implementation

    @tool()
    def tool3(self, ...) -> type:
        """
        <full docstring for this method>
        """
        ... #implementation

Methods should be type annotated, and contain matching docstrings, same as with function tools (noting that you should not mention the self parameter in the docstring).

SIR Toolset Example

Here's an example toolset for managing/running SIR simulations, adjusting simulation parameters, etc.

from archytas.tool_utils import toolset, tool


@toolset()
class ModelSimulation:
    """
    Simple example of a SIR model simulation
    """
    def __init__(self, dt=0.1):
        self._default_parameters = {'beta': 0.002, 'gamma': 0.1, 'S': 990, 'I': 10, 'R': 0}
        self.parameters = self._default_parameters.copy()
        self.dt = dt

    @tool()
    def get_model_parameters(self) -> dict:
        """
        Get the model parameters

        Returns:
            dict: The model parameters in the form {param0: value0, param1: value1, ...}

        """
        return self.parameters

    @tool()
    def set_model_parameters(self, update:dict):
        """
        Set some or all of the model parameters

        Args:
            update (dict): The parameters to update. Should be a dict of the form {param0: value0, param1: value1, ...}. Only the parameters specified will be updated.
        """
        self.parameters.update(update)

    @tool()
    def run_model(self, steps:int=100) -> dict:
        """
        Run the model for a number of steps

        Args:
            steps (int): The number of steps to run the model for. Defaults to 100.

        Returns:
            dict: The model results in the form {param0: value0, param1: value1, ...}
        """
        S_new, I_new, R_new = self.parameters['S'], self.parameters['I'], self.parameters['R']
        beta, gamma = self.parameters['beta'], self.parameters['gamma']
        population = S_new + I_new + R_new

        for _ in range(steps):
            S_old, I_old, R_old = S_new, I_new, R_new
            dS = -beta * S_old * I_old
            dI = beta * S_old * I_old - gamma * I_old
            dR = gamma * I_old

            S_new = max(0, min(S_old + self.dt*dS, population))
            I_new = max(0, min(I_old + self.dt*dI, population))
            R_new = max(0, min(R_old + self.dt*dR, population))

            # Ensure the total population remains constant
            total_error = population - (S_new + I_new + R_new)
            R_new += total_error

        self.parameters['S'], self.parameters['I'], self.parameters['R'] = S_new, I_new, R_new
        return self.parameters

    @tool()
    def reset_model(self):
        """
        Reset the model to the initial parameters
        """
        self.parameters = self._default_parameters.copy()

Notice that the class contains persistent data, as well as multiple methods that the LLM could call.

When instantiating an agent with this toolset, or any toolset, you have the option to pass the class constructor directly, or create your own instance which you pass in.

# directly pass the class constructor to tools
tools = [ModelSimulation]
agent = ReActAgent(tools=tools, verbose=True)

This is convenient if the default instantiation of the class is adequate for your use case. To use this approach, your class must be able to be instantiated with zero arguments (i.e. all arguments have a default value)

# create an instance and modify before passing in to agent
sim = ModelSimulation(dt=0.05)
sim._default_parameters = {'beta': 0.003, 'gamma': 0.2, 'S': 9990, 'I': 10, 'R': 0}
sim.reset_model()

tools = [sim]
agent = ReActAgent(tools=tools, verbose=True)

this is useful if you want to override the default initialization of the class.

@toolset decorator

The toolset decorator serves two main functions:

  1. Create the prompt for the LLM for the toolset and all its contained tools.

    For the model simulation tool above, the following prompt gets generated:

    ModelSimulation (class):
        Simple example of a SIR model simulation
    
        methods:
            get_model_parameters:
                Get the model parameters
    
                _input_: None
                _output_: (dict) The model parameters in the form {param0: value0, param1: value1, ...}
    
            reset_model:
                Reset the model to the initial parameters
    
                _input_: None
                _output_: None
            
            run_model:
                Run the model for a number of steps
    
                _input_: (int, optional) The number of steps to run the model for. Defaults to 100.
                _output_: (dict) The model results in the form {param0: value0, param1: value1, ...}
    
            set_model_parameters:
                Set some or all of the model parameters
    
                _input_: a json object with the following fields:
                {
                    "update": # (dict) The parameters to update. Should be a dict of the form {param0: value0, param1: value1, ...}. Only the parameters specified will be updated.
                }
                _output_: None
    
  2. managing calling the methods, and passing results back to the LLM

    • this process is very similar to the way it's handled for function tools. The main difference is that internally, the @toolset decorator keeps an instance of the class, and the decorator routes the calls to the correct method based on the LLM's selected actions.
    • The LLM refers to tools in a toolset via dot notation, so for the ModelSimulation toolset, the LLM can refer to the following tools:
      • ModelSimulation.get_model_parameters
      • ModelSimulation.run_model
      • ModelSimulation.set_model_parameters
      • ModelSimulation.reset_model

    An example call the LLM might make to the toolset could look like this:

    {
        "thought": "The user wants me to set the beta parameter to 0.004",
        "tool": "ModelSimulation.set_model_parameters",
        "tool_input": {"beta": 0.004}
    }
    

Clone this wiki locally