mirror of
https://github.com/Hopiu/llm.git
synced 2026-03-16 20:50:25 +00:00
llm chat --tool and --functions (#1062)
* Tool support for llm chat, closes #1004
This commit is contained in:
parent
e48a5b9f11
commit
bd2180df7d
8 changed files with 152 additions and 9 deletions
|
|
@ -173,6 +173,13 @@ Options:
|
|||
-d, --database FILE Path to log database
|
||||
--no-stream Do not stream output
|
||||
--key TEXT API key to use
|
||||
-T, --tool TEXT Name of a tool to make available to the model
|
||||
--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
|
||||
--cl, --chain-limit INTEGER How many chained tool responses to allow, default
|
||||
5, set 0 for unlimited
|
||||
--help Show this message and exit.
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -162,6 +162,17 @@ Approve tool call? [y/N]:
|
|||
```
|
||||
The `--functions` option can be passed more than once, and can also point to the filename of a `.py` file containing one or more functions.
|
||||
|
||||
If you have any tools that have been made available via plugins you can add them to the prompt using `--tool/-T` option. For example, using [llm-tools-simpleeval](https://github.com/simonw/llm-tools-simpleeval) like this:
|
||||
|
||||
```bash
|
||||
llm install llm-tools-simpleeval
|
||||
llm --tool simple_eval "4444 * 233423" --td
|
||||
```
|
||||
Run this command to see a list of available tools from plugins:
|
||||
```bash
|
||||
llm tools
|
||||
```
|
||||
|
||||
(usage-extract-fenced-code)=
|
||||
### Extracting fenced code blocks
|
||||
|
||||
|
|
@ -416,6 +427,8 @@ Type '!edit' to open your default editor and modify the prompt.
|
|||
> !edit
|
||||
```
|
||||
|
||||
`llm chat` takes the same `--tool/-T` and `--functions` options as `llm prompt`. You can use this to start a chat with the specified {ref}`tools <usage-tools>` enabled.
|
||||
|
||||
## Listing available models
|
||||
|
||||
The `llm models` command lists every model that can be used with LLM, along with their aliases. This includes models that have been installed using {ref}`plugins <plugins>`.
|
||||
|
|
|
|||
56
llm/cli.py
56
llm/cli.py
|
|
@ -909,6 +909,42 @@ def prompt(
|
|||
)
|
||||
@click.option("--no-stream", is_flag=True, help="Do not stream output")
|
||||
@click.option("--key", help="API key to use")
|
||||
@click.option(
|
||||
"tools",
|
||||
"-T",
|
||||
"--tool",
|
||||
multiple=True,
|
||||
help="Name of a tool to make available to the model",
|
||||
)
|
||||
@click.option(
|
||||
"python_tools",
|
||||
"--functions",
|
||||
help="Python code block or file path defining functions to register as tools",
|
||||
multiple=True,
|
||||
)
|
||||
@click.option(
|
||||
"tools_debug",
|
||||
"--td",
|
||||
"--tools-debug",
|
||||
is_flag=True,
|
||||
help="Show full details of tool executions",
|
||||
envvar="LLM_TOOLS_DEBUG",
|
||||
)
|
||||
@click.option(
|
||||
"tools_approve",
|
||||
"--ta",
|
||||
"--tools-approve",
|
||||
is_flag=True,
|
||||
help="Manually approve every tool execution",
|
||||
)
|
||||
@click.option(
|
||||
"chain_limit",
|
||||
"--cl",
|
||||
"--chain-limit",
|
||||
type=int,
|
||||
default=5,
|
||||
help="How many chained tool responses to allow, default 5, set 0 for unlimited",
|
||||
)
|
||||
def chat(
|
||||
system,
|
||||
model_id,
|
||||
|
|
@ -920,6 +956,11 @@ def chat(
|
|||
no_stream,
|
||||
key,
|
||||
database,
|
||||
tools,
|
||||
python_tools,
|
||||
tools_debug,
|
||||
tools_approve,
|
||||
chain_limit,
|
||||
):
|
||||
"""
|
||||
Hold an ongoing chat with a model.
|
||||
|
|
@ -987,7 +1028,18 @@ def chat(
|
|||
raise click.ClickException(render_errors(ex.errors()))
|
||||
|
||||
kwargs = {}
|
||||
kwargs.update(validated_options)
|
||||
if validated_options:
|
||||
kwargs["options"] = validated_options
|
||||
|
||||
tool_functions = _gather_tools(tools, python_tools)
|
||||
|
||||
if tool_functions:
|
||||
kwargs["chain_limit"] = chain_limit
|
||||
if tools_debug:
|
||||
kwargs["after_call"] = _debug_tool_call
|
||||
if tools_approve:
|
||||
kwargs["before_call"] = _approve_tool_call
|
||||
kwargs["tools"] = tool_functions
|
||||
|
||||
should_stream = model.can_stream and not no_stream
|
||||
if not should_stream:
|
||||
|
|
@ -1042,7 +1094,7 @@ def chat(
|
|||
prompt = new_prompt
|
||||
if prompt.strip() in ("exit", "quit"):
|
||||
break
|
||||
response = conversation.prompt(prompt, system=system, **kwargs)
|
||||
response = conversation.chain(prompt, system=system, **kwargs)
|
||||
# System prompt only sent for the first message:
|
||||
system = None
|
||||
for chunk in response:
|
||||
|
|
|
|||
|
|
@ -332,7 +332,7 @@ class Conversation(_BaseConversation):
|
|||
details: bool = False,
|
||||
key: Optional[str] = None,
|
||||
options: Optional[dict] = None,
|
||||
):
|
||||
) -> "ChainResponse":
|
||||
self.model._validate_attachments(attachments)
|
||||
return ChainResponse(
|
||||
Prompt(
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ test = [
|
|||
"types-click",
|
||||
"types-PyYAML",
|
||||
"types-setuptools",
|
||||
"llm-echo==0.3a1",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import pytest
|
|||
import sqlite_utils
|
||||
import json
|
||||
import llm
|
||||
import llm_echo
|
||||
from llm.plugins import pm
|
||||
from pydantic import Field
|
||||
from pytest_httpx import IteratorStream
|
||||
|
|
@ -233,19 +234,20 @@ def register_embed_demo_model(embed_demo, mock_model, async_mock_model):
|
|||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def register_echo_model():
|
||||
class SimpleEchoModelPlugin:
|
||||
__name__ = "SimpleEchoModelPlugin"
|
||||
def register_echo_models():
|
||||
class EchoModelsPlugin:
|
||||
__name__ = "EchoModelsPlugin"
|
||||
|
||||
@llm.hookimpl
|
||||
def register_models(self, register):
|
||||
register(SimpleEchoModel())
|
||||
register(llm_echo.Echo(), llm_echo.EchoAsync())
|
||||
|
||||
pm.register(SimpleEchoModelPlugin(), name="undo-SimpleEchoModelPlugin")
|
||||
pm.register(EchoModelsPlugin(), name="undo-EchoModelsPlugin")
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
pm.unregister(name="undo-SimpleEchoModelPlugin")
|
||||
pm.unregister(name="undo-EchoModelsPlugin")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
from click.testing import CliRunner
|
||||
import json
|
||||
import llm.cli
|
||||
from unittest.mock import ANY
|
||||
import pytest
|
||||
import sys
|
||||
import sqlite_utils
|
||||
import textwrap
|
||||
|
||||
|
||||
@pytest.mark.xfail(sys.platform == "win32", reason="Expected to fail on Windows")
|
||||
|
|
@ -265,3 +267,69 @@ def test_llm_chat_creates_log_database(tmpdir, monkeypatch, custom_database_path
|
|||
assert (user_path / "logs.db").exists()
|
||||
db_path = str(user_path / "logs.db")
|
||||
assert sqlite_utils.Database(db_path)["responses"].count == 2
|
||||
|
||||
|
||||
@pytest.mark.xfail(sys.platform == "win32", reason="Expected to fail on Windows")
|
||||
def test_chat_tools(logs_db):
|
||||
runner = CliRunner()
|
||||
functions = textwrap.dedent(
|
||||
"""
|
||||
def upper(text: str) -> str:
|
||||
"Convert text to upper case"
|
||||
return text.upper()
|
||||
"""
|
||||
)
|
||||
result = runner.invoke(
|
||||
llm.cli.cli,
|
||||
["chat", "-m", "echo", "--functions", functions],
|
||||
input="\n".join(
|
||||
[
|
||||
json.dumps(
|
||||
{
|
||||
"prompt": "Convert hello to uppercase",
|
||||
"tool_calls": [
|
||||
{"name": "upper", "arguments": {"text": "hello"}}
|
||||
],
|
||||
}
|
||||
),
|
||||
"quit",
|
||||
]
|
||||
),
|
||||
catch_exceptions=False,
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert result.output == (
|
||||
"Chatting with echo\n"
|
||||
"Type 'exit' or 'quit' to exit\n"
|
||||
"Type '!multi' to enter multiple lines, then '!end' to finish\n"
|
||||
"Type '!edit' to open your default editor and modify the prompt\n"
|
||||
'> {"prompt": "Convert hello to uppercase", "tool_calls": [{"name": "upper", '
|
||||
'"arguments": {"text": "hello"}}]}\n'
|
||||
"{\n"
|
||||
' "prompt": "Convert hello to uppercase",\n'
|
||||
' "system": "",\n'
|
||||
' "attachments": [],\n'
|
||||
' "stream": true,\n'
|
||||
' "previous": []\n'
|
||||
"}{\n"
|
||||
' "prompt": "",\n'
|
||||
' "system": "",\n'
|
||||
' "attachments": [],\n'
|
||||
' "stream": true,\n'
|
||||
' "previous": [\n'
|
||||
" {\n"
|
||||
' "prompt": "{\\"prompt\\": \\"Convert hello to uppercase\\", '
|
||||
'\\"tool_calls\\": [{\\"name\\": \\"upper\\", \\"arguments\\": {\\"text\\": '
|
||||
'\\"hello\\"}}]}"\n'
|
||||
" }\n"
|
||||
" ],\n"
|
||||
' "tool_results": [\n'
|
||||
" {\n"
|
||||
' "name": "upper",\n'
|
||||
' "output": "HELLO",\n'
|
||||
' "tool_call_id": null\n'
|
||||
" }\n"
|
||||
" ]\n"
|
||||
"}\n"
|
||||
"> quit\n"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -297,11 +297,11 @@ def test_plugins_command():
|
|||
result = runner.invoke(cli.cli, ["plugins"])
|
||||
assert result.exit_code == 0
|
||||
expected = [
|
||||
{"name": "EchoModelsPlugin", "hooks": ["register_models"]},
|
||||
{
|
||||
"name": "MockModelsPlugin",
|
||||
"hooks": ["register_embedding_models", "register_models"],
|
||||
},
|
||||
{"name": "SimpleEchoModelPlugin", "hooks": ["register_models"]},
|
||||
]
|
||||
actual = json.loads(result.output)
|
||||
actual.sort(key=lambda p: p["name"])
|
||||
|
|
|
|||
Loading…
Reference in a new issue