--tools is now --functions, can be path, can be multiple

Closes #1016
This commit is contained in:
Simon Willison 2025-05-13 10:18:59 -07:00
parent 1efb14f294
commit a880c123bd
4 changed files with 39 additions and 18 deletions

View file

@ -127,8 +127,8 @@ Options:
Attachment with explicit mimetype,
--at image.jpg image/jpeg
-T, --tool TEXT Name of a tool to make available to the model
--tools TEXT Python code block defining functions to
register as tools
--functions TEXT Python code block or file path defining
functions to register as tools
--td, --tools-debug Show full details of tool executions
--ta, --tools-approve Manually approve every tool execution
-o, --option <TEXT TEXT>... key/value options for the model
@ -628,9 +628,10 @@ Usage: llm tools list [OPTIONS]
List available tools that have been provided by plugins
Options:
--json Output as JSON
--tools TEXT Python code block defining functions to register as tools
--help Show this message and exit.
--json Output as JSON
--functions TEXT Python code block or file path defining functions to
register as tools
--help Show this message and exit.
```
(help-aliases)=

View file

@ -20,4 +20,4 @@ In LLM every tool is a defined as a Python function. The function can take any n
Tool functions should include a docstring that describes what the function does. This docstring will become the description that is passed to the model.
The Python API can accept functions directly. The command-line interface has two ways for tools to be defined: via plugins that implement the {ref}`register_tools() plugin hook <plugin-hooks-register-tools>`, or directly on the command-line using the `--tools` argument to specify a block of Python code defining one or more functions.
The Python API can accept functions directly. The command-line interface has two ways for tools to be defined: via plugins that implement the {ref}`register_tools() plugin hook <plugin-hooks-register-tools>`, or directly on the command-line using the `--functions` argument to specify a block of Python code defining one or more functions - or a path to a Python file containing the same.

View file

@ -344,8 +344,9 @@ def cli():
)
@click.option(
"python_tools",
"--tools",
help="Python code block defining functions to register as tools",
"--functions",
help="Python code block or file path defining functions to register as tools",
multiple=True,
)
@click.option(
"tools_debug",
@ -757,7 +758,8 @@ def prompt(
extra_tools = []
if python_tools:
extra_tools = _tools_from_code(python_tools)
for code_or_path in python_tools:
extra_tools = _tools_from_code(code_or_path)
if tools or python_tools:
prompt_method = conversation.chain
@ -2286,15 +2288,17 @@ def tools():
@click.option("json_", "--json", is_flag=True, help="Output as JSON")
@click.option(
"python_tools",
"--tools",
help="Python code block defining functions to register as tools",
"--functions",
help="Python code block or file path defining functions to register as tools",
multiple=True,
)
def tools_list(json_, python_tools):
"List available tools that have been provided by plugins"
tools = get_tools()
if python_tools:
for tool in _tools_from_code(python_tools):
tools[tool.name] = tool
for code_or_path in python_tools:
for tool in _tools_from_code(code_or_path):
tools[tool.name] = tool
if json_:
click.echo(
json.dumps(
@ -3565,16 +3569,21 @@ def load_template(name: str) -> Template:
return _parse_yaml_template(name, content)
def _tools_from_code(code: str) -> List[Tool]:
def _tools_from_code(code_or_path: str) -> List[Tool]:
"""
Treat all Python functions in the code as tools
"""
if "\n" not in code_or_path and code_or_path.endswith(".py"):
try:
code_or_path = pathlib.Path(code_or_path).read_text()
except FileNotFoundError:
raise click.ClickException("File not found: {}".format(code_or_path))
globals = {}
tools = []
try:
exec(code, globals)
exec(code_or_path, globals)
except SyntaxError as ex:
raise click.ClickException("Error in --tools definition: {}".format(ex))
raise click.ClickException("Error in --functions definition: {}".format(ex))
# Register all callables in the locals dict:
for name, value in globals.items():
if callable(value) and not name.startswith("_"):

View file

@ -191,7 +191,7 @@ def test_register_fragment_loaders(logs_db, httpx_mock):
]
def test_register_tools():
def test_register_tools(tmpdir):
def upper(text: str) -> str:
"""Convert text to uppercase."""
return text.upper()
@ -271,11 +271,22 @@ def test_register_tools():
},
}
# And test the --tools option
functions_path = str(tmpdir / "functions.py")
with open(functions_path, "w") as fp:
fp.write("def example(s: str, i: int):\n return s + '-' + str(i)")
result3 = runner.invoke(
cli.cli, ["tools", "--tools", "def reverse(s: str): return s[::-1]"]
cli.cli,
[
"tools",
"--functions",
"def reverse(s: str): return s[::-1]",
"--functions",
functions_path,
],
)
assert result3.exit_code == 0
assert "reverse(s: str)" in result3.output
assert "example(s: str, i: int)" in result3.output
finally:
plugins.pm.unregister(name="ToolsPlugin")
assert llm.get_tools() == {}