Skip to content

Toolbox.add_tool() mechanism #1185

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

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft

Toolbox.add_tool() mechanism #1185

wants to merge 2 commits into from

Conversation

simonw
Copy link
Owner

@simonw simonw commented Jun 20, 2025

Refs:

TODO:

  • Decide if things added by add_tools() should be normal functions or if they should be transformed into methods which can do something useful by using self to access original class configuration.
  • Add documentation

@simonw
Copy link
Owner Author

simonw commented Jun 20, 2025

I need to build an example Toolbox that's dynamically configured with extra tools based on the URL. I don't want to implement the whole of MCP but maybe there's a simpler version. I want something that adds tools dynamically based on an argument passed to the constructor.

I'm going to try a ShellTools one which is configured with a list of shell commands (e.g. ["ffmpeg", "jq']) and provides access to those tools, each one getting a separate method.

@simonw
Copy link
Owner Author

simonw commented Jun 20, 2025

First working version of that:

import llm
import subprocess


def make_caller(command):
    def call_tool(args: list[str]) -> str:
        try:
            result = subprocess.run(
                [command] + args, capture_output=True, text=True, check=True
            )
            return result.stdout[:2000]
        except subprocess.CalledProcessError as e:
            error_msg = (
                e.stderr.strip()
                if e.stderr
                else f"Command failed with exit code {e.returncode}"
            )
            return f"Error: {error_msg[:2000]}"

    return call_tool


class ShellCommands(llm.Toolbox):
    def __init__(self, shell_commands: list[str]):
        self.shell_commands = shell_commands
        for command in self.shell_commands:
            self.add_tool(
                llm.Tool.function(
                    make_caller(command),
                    name=command,
                    description="Call this with an array of strings to be passed as options to the command. Command help:\n\n"
                    + subprocess.check_output([command, "--help"]).decode("utf-8"),
                )
            )


@llm.hookimpl
def register_tools(register):
    register(ShellCommands)

And now:

llm -T 'ShellCommands(["ffmpeg"])' 'Save a m4a version of russian-pelican-in-spanish.mp3' --ta --td

Outputs:

Tool call: ffmpeg({'args': ['-i', 'russian-pelican-in-spanish.mp3', '-c:a', 'aac', '-b:a', '192k', '-vn', 'russian-pelican-in-spanish.m4a']})
Approve tool call? [y/N]: y

Tool call: ffmpeg({'args': ['-i', 'russian-pelican-in-spanish.mp3', '-c:a', 'aac', '-b:a', '192k', '-vn', 'russian-pelican-in-spanish.m4a']})


I have converted the file "russian-pelican-in-spanish.mp3" to an m4a version with the filename "russian-pelican-in-spanish.m4a". Let me know if you need anything else.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant