diff --git a/docs/help.md b/docs/help.md index 289125f..350afca 100644 --- a/docs/help.md +++ b/docs/help.md @@ -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 ... 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)= diff --git a/docs/tools.md b/docs/tools.md index e1d09c4..66de5a8 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -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 `, or directly on the command-line using the `--tools` argument to specify a block of Python code defining one or more functions. \ No newline at end of file +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 `, 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. \ No newline at end of file diff --git a/llm/cli.py b/llm/cli.py index c22ef1c..69f501e 100644 --- a/llm/cli.py +++ b/llm/cli.py @@ -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("_"): diff --git a/tests/test_plugins.py b/tests/test_plugins.py index dddf185..1cd42ee 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -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() == {}