Originally created by @michaelpoluektov on GitHub (Jul 30, 2024).
Is your feature request related to a problem? Please describe.
There is a lot of unnecessary complexity associated with the current way tools are handled by default. As of right now, OWUI does the following:
Call all inlets (if filter functions are present)
Handle tools (if any toolkit are activated):
For each toolkit, the model may chose to call a single function.
The output of that function is added to the context
Call the file handler (if no tools override it)
Call all outlets (if filter functions are present)
Describe the solution you'd like
I would like to eventually make the following improvements, and I'm wondering if anyone has any opinions/objections to how this should be handled.
Rather than iterating through each toolkit and individually deciding if tools should be called, the model should be given all definitions of activated tools at once, and call all selected tools. This solves the following problems with the current approach:
We are redundantly making an LLM call for every single toolkit the user has activated, which increases latency
There is no pre-determined order in which the toolkits are called, so the model might decide to call a less suitable tool first
If the model decides to call a tool from 2 different toolkits, the output of the previous call will be overwritten
Tool definitions and functions should be available to use in functions via custom parameters, such as __tools__: list[dict]. Example:
__tools__=[{"callable":tool_callable,"definition":{"name":"search_docs","description":"Search documentation for a given query.","parameters":{"type":"object","properties":{"query":{"type":"str","description":"query"}}},"required":["query"]}}]
When generating tool calls, a proper tool calling API should be used (available in the latest versions of Ollama) to ensure tool calls have the valid parameters.
When handling the outputs of tool calls, they should be appended as messages with "tool" role if the model supports it, rather than always adding it directly to the system prompt. Tool messages should be properly rendered in the frontend, ideally with indicators to show that tools are being executed.
For models that support it, tools should be included with each message rather than being pre-pended to each generation. This would allow the model to call multiple tools in a single message, as well as to "reason" before tools are called.
Originally created by @michaelpoluektov on GitHub (Jul 30, 2024).
**Is your feature request related to a problem? Please describe.**
There is a lot of unnecessary complexity associated with the current way tools are handled by default. As of right now, OWUI does the following:
- Call all inlets (if filter functions are present)
- Handle tools (if any toolkit are activated):
* For each toolkit, the model may chose to call a single function.
* The output of that function is added to the context
- Call the file handler (if no tools override it)
- Call all outlets (if filter functions are present)
**Describe the solution you'd like**
I would like to eventually make the following improvements, and I'm wondering if anyone has any opinions/objections to how this should be handled.
- [x] Rather than iterating through each toolkit and individually deciding if tools should be called, the model should be given all definitions of activated tools at once, and call all selected tools. This solves the following problems with the current approach:
* We are redundantly making an LLM call for every single toolkit the user has activated, which increases latency
* There is no pre-determined order in which the toolkits are called, so the model might decide to call a less suitable tool first
* If the model decides to call a tool from 2 different toolkits, the output of the previous call will be overwritten
- [x] Tool definitions and functions should be available to use in functions via custom parameters, such as `__tools__: list[dict]`. Example:
```python
__tools__ = [
{
"callable": tool_callable,
"definition": {
"name": "search_docs",
"description": "Search documentation for a given query.",
"parameters": {"type": "object", "properties": {"query": {"type": "str","description": "query"}}},
"required": ["query"]
}
}
]
```
Tracked here: https://github.com/open-webui/open-webui/issues/3718
- [ ] When generating tool calls, a proper tool calling API should be used (available in the latest versions of Ollama) to ensure tool calls have the valid parameters.
- [ ] When handling the outputs of tool calls, they should be appended as messages with "tool" role if the model supports it, rather than always adding it directly to the system prompt. Tool messages should be properly rendered in the frontend, ideally with indicators to show that tools are being executed.
- [x] For models that support it, tools should be included with each message rather than being pre-pended to each generation. This would allow the model to call multiple tools in a single message, as well as to "reason" before tools are called.
When generating tool calls, a proper tool calling API should be used (available in the latest versions of Ollama) to ensure tool calls have the valid parameters.
I believe this current limitation makes it difficult for models to call tools that need more elaborate inputs, such as for code execution where the argument (the code to run) can be quite long and complicated. Even in simple cases, I'm sometimes running into cases where the model tries to generate a tool call but adds garbage tokens before the tool call's structured JSON, e.g. "<|python_tag|>" in `<|python_tag|>{"name": "run_bash_command", "parameters": {"bash_command": "date; dmesg"}}.
try:result=json.loads(content)exceptExceptionase:print(f"JSON exception: {e}; content tried to load: {content}")raisee
It (sometimes) prints:
ERROR [main] Error: Expecting value: line 1 column 1 (char 0)
Traceback (most recent call last):
File "/app/backend/main.py", line 425, in chat_completion_tools_handler
raise e
File "/app/backend/main.py", line 422, in chat_completion_tools_handler
result = json.loads(content)
^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/json/__init__.py", line 346, in loads
return _default_decoder.decode(s)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/json/decoder.py", line 337, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/json/decoder.py", line 355, in raw_decode
raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
JSON exception: Expecting value: line 1 column 1 (char 0); content tried to load: <|python_tag|>{"name": "run_bash_command", "parameters": {"bash_command": "date; dmesg"}}
@EtiennePerot commented on GitHub (Sep 1, 2024):
> * [ ] When generating tool calls, a proper tool calling API should be used (available in the latest versions of Ollama) to ensure tool calls have the valid parameters.
I believe this current limitation makes it difficult for models to call tools that need more elaborate inputs, such as for code execution where the argument (the code to run) can be quite long and complicated. Even in simple cases, I'm sometimes running into cases where the model tries to generate a tool call but adds garbage tokens before the tool call's structured JSON, e.g. "`<|python_tag|>`" in `<|python_tag|>{"name": "run_bash_command", "parameters": {"bash_command": "date; dmesg"}}.
Specifically, if I add logging around this line:
https://github.com/open-webui/open-webui/blob/f4df49e600a36ea1616f4d0dc6e5c748fc4dbb74/backend/main.py#L415
like so:
```python
try:
result = json.loads(content)
except Exception as e:
print(f"JSON exception: {e}; content tried to load: {content}")
raise e
```
It (sometimes) prints:
```
ERROR [main] Error: Expecting value: line 1 column 1 (char 0)
Traceback (most recent call last):
File "/app/backend/main.py", line 425, in chat_completion_tools_handler
raise e
File "/app/backend/main.py", line 422, in chat_completion_tools_handler
result = json.loads(content)
^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/json/__init__.py", line 346, in loads
return _default_decoder.decode(s)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/json/decoder.py", line 337, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/json/decoder.py", line 355, in raw_decode
raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
JSON exception: Expecting value: line 1 column 1 (char 0); content tried to load: <|python_tag|>{"name": "run_bash_command", "parameters": {"bash_command": "date; dmesg"}}
```
@michaelpoluektov commented on GitHub (Sep 2, 2024):
The way I would implement a Python tool would be to define a function that takes no external parameters other than __messages__ which calls eval on the last snippet of code wrapped in a ```python tag. That should resolve your issue.
@michaelpoluektov commented on GitHub (Sep 2, 2024):
The way I would implement a Python tool would be to define a function that takes no external parameters other than `__messages__` which calls `eval` on the last snippet of code wrapped in a ` ```python ` tag. That should resolve your issue.
@michaelpoluektov I think that's a slightly different use-case than the one I'm trying to solve. The intent of having it as a tool is to let the LLM decide and query information it needs (without overloading the system message with too many tools) as a sort of knowledge-gathering step prior to answering the user's prompt, similar to other tools like web searching.
By contrast, the way the function you describe would work would be more like Open WebUI's existing Pyodide integration with the "Run" button showing up on Python code blocks. It would act on visible Python code blocks that the user would explicitly have to ask for.
@EtiennePerot commented on GitHub (Sep 2, 2024):
@michaelpoluektov I think that's a slightly different use-case than the one I'm trying to solve. The intent of having it as a tool is to let the LLM decide and query information it needs (without overloading the system message with too many tools) as a sort of knowledge-gathering step prior to answering the user's prompt, similar to other tools like web searching.
By contrast, the way the function you describe would work would be more like Open WebUI's existing Pyodide integration with the "Run" button showing up on Python code blocks. It would act on visible Python code blocks that the user would explicitly have to ask for.
@michaelpoluektov commented on GitHub (Sep 2, 2024):
The goal is eventually not to have tool outputs only be pre-pended on each query, but potentially called mid-way through the generation. Similar to https://openwebui.com/f/michaelpoluektov/openai_react/ (I'm waiting on Ollama to support streaming tool calls to make a PR implementing this natively)
This would allow you to run the code in on the backend (through something like Terrarium for security) and pipe the result back in to the LLM before the rest of the response is generated. The only difference is that the user would see the code but that can also easily be solved with functions magic if it's not desired
@michaelpoluektov commented on GitHub (Sep 2, 2024):
The goal is eventually not to have tool outputs only be pre-pended on each query, but potentially called mid-way through the generation. Similar to https://openwebui.com/f/michaelpoluektov/openai_react/ (I'm waiting on Ollama to support streaming tool calls to make a PR implementing this natively)
This would allow you to run the code in on the backend (through something like Terrarium for security) and pipe the result back in to the LLM before the rest of the response is generated. The only difference is that the user would see the code but that can also easily be solved with functions magic if it's not desired
Makes sense, thanks. I didn't realize ollama's tool support was still so primitive.
For those interested in tracking progress on the ollama side, I believe this is tracked in https://github.com/ollama/ollama/issues/5796.
In the meantime, I have also implemented your suggestion of a function and uploaded it here.
@EtiennePerot commented on GitHub (Sep 3, 2024):
Makes sense, thanks. I didn't realize ollama's tool support was still so primitive.
For those interested in tracking progress on the ollama side, I believe this is tracked in https://github.com/ollama/ollama/issues/5796.
In the meantime, I have also implemented your suggestion of a function and uploaded it [here](https://github.com/EtiennePerot/open-webui-code-execution).
I have tested it witth 4omini, deepseek and qwen2.5, and they work fine.
I basically peeks into LLM api responses and if there are any tool calls, it stops forwarding the data to the client, aggregates the deltas and executes what the LLM wants to run.
In my opinion, this way of running tools has enormous advantage of being able to use different tools together and a same tool may be used several times so the LLM can correct its own mistakes. (which is crucial for running-code enabling tools).
Before working on polishing it, I have a couple questions/concerns:
is this a viable path? If it gets a bug free status, would be included in the main branch ( I guess this is a question for @tjbck mainly)
I can't test in ollama, my CPU resources are extremely limited. Is there any way to access any ollama based API to make it work for ollama (AFAIK it has tool support capabilities https://ollama.com/blog/tool-support)
I post here a comparison using the same prompt. In the first case, the LLM is able to self correct and complete the task while in the other starts hallucinating a response:
MULTIPLE CALLS VERSION
USER
list the files on dir "test"
look at the names and choose the one which has filename different from the others (you'll know when you see them)
inspect the contents of that file and extract the python code
execute code.
Explain what you are doing after every step
ASSISTANT
Execution: 2024-10-13T13:26:57.464090
import os\nprint(os.listdir('test'))
Output:
File "/tmp/app.py", line 1
import os\nprint(os.listdir('test'))
^
SyntaxError: unexpected character after line continuation character
Execution: 2024-10-13T13:27:02.660632
import os\nprint(os.listdir('test'))
Output:
File "/tmp/app.py", line 1
import os\nprint(os.listdir('test'))
^
SyntaxError: unexpected character after line continuation character
Execution: 2024-10-13T13:27:07.640838
import os
print(os.listdir('test'))
Output:
['d', 'b', 'apple', 'a', 'f', 'c', 'e']
Execution: 2024-10-13T13:27:14.290234
with open('test/apple', 'r') as file:\n content = file.read()\nprint(content)
Output:
File "/tmp/app.py", line 1
with open('test/apple', 'r') as file:\n content = file.read()\nprint(content)
^
SyntaxError: unexpected character after line continuation character
Execution: 2024-10-13T13:27:19.597312
with open('test/apple', 'r') as file:
content = file.read()
print(content)
Output:
dd
fkd
ldkfkdfd
print("hello world")
Execution: 2024-10-13T13:27:26.580866
print('hello world')
Output:
hello world
The steps I've taken are as follows:
I listed the files in the test directory. The files are: d, b, apple, a, f, c, and e.
I identified the file apple as the one with a name that is different from the others.
I inspected the contents of the apple file and found the following Python code:
ddfkdldkfkdfdprint("hello world")
I executed the Python code extracted from the apple file, and it printed:
hello world
SINGLE CALL VERSION
USER
list the files on dir "test"
look at the names and choose the one which has filename different from the others (you'll know when you see them)
inspect the contents of that file and extract the python code
execute code.
Explain what you are doing after every step
ASSISTANT
Execution: 2024-10-13T11:35:26.091281
import os\n# Step 1: List the files in the directory 'test'\nfiles = os.listdir('test')\nprint('Files in directory:\n', files)\n# Step 2: Identify the file with a name different from the others\nodd_file = None\nfor file in files:\n if file.endswith('.py') or (not file.startswith('file_') and not file.endswith('.txt')):\n odd_file = file\n break\nprint('The file with a unique name is:', odd_file)\n# Step 3: Read the content of the identified file\nif odd_file:\n with open(f'test/{odd_file}', 'r') as f:\n file_content = f.read()\n print('Content of the file:\n', file_content)\n# Step 4: Execute the Python code found in the file\nif file_content:\n exec(file_content)\nelse:\n print('No content to execute.')
Output:
File "/tmp/app.py", line 1
import os\n# Step 1: List the files in the directory 'test'\nfiles = os.listdir('test')\nprint('Files in directory:\n', files)\n# Step 2: Identify the file with a name different from the others\nodd_file = None\nfor file in files:\n if file.endswith('.py') or (not file.startswith('file_') and not file.endswith('.txt')):\n odd_file = file\n break\nprint('The file with a unique name is:', odd_file)\n# Step 3: Read the content of the identified file\nif odd_file:\n with open(f'test/{odd_file}', 'r') as f:\n file_content = f.read()\n print('Content of the file:\n', file_content)\n# Step 4: Execute the Python code found in the file\nif file_content:\n exec(file_content)\nelse:\n print('No content to execute.')
^
SyntaxError: unexpected character after line continuation character
Sure, I'll go through the steps one by one and explain what I'm doing at each step.
Step 1: List the files in the directory "test"
I will list all the files in the "test" directory to see what we're working with. This will help us identify which file has a unique name.
importos# Step 1: List the files in the directory 'test'files=os.listdir('test')print('Files in directory:\n',files)
Let's execute this code to list the files.
importos# Step 1: List the files in the directory 'test'files=os.listdir('test')print('Files in directory:\n',files)
Step 2: Identify the file with a unique name
Once we have the list of files, I will inspect the names to find the one that is different from the others. Typically, a unique file might have a different extension, a different naming pattern, or some other distinguishing feature.
# Step 2: Identify the file with a name different from the othersodd_file=Noneforfileinfiles:iffile.endswith('.py')or(notfile.startswith('file_')andnotfile.endswith('.txt')):odd_file=filebreakprint('The file with a unique name is:',odd_file)
....
@smonux commented on GitHub (Oct 13, 2024):
I have working on lately and I have working version (surely still has bugs and needs some cleaning). If anyone is interested, it's here:
https://github.com/smonux/open-webui/blob/dev-apitools/backend/open_webui/main.py
I have tested it witth 4omini, deepseek and qwen2.5, and they work fine.
I basically peeks into LLM api responses and if there are any tool calls, it stops forwarding the data to the client, aggregates the deltas and executes what the LLM wants to run.
In my opinion, this way of running tools has enormous advantage of being able to use different tools together and a same tool may be used several times **so the LLM can correct its own mistakes.** (which is crucial for running-code enabling tools).
Before working on polishing it, I have a couple questions/concerns:
- is this a viable path? If it gets a bug free status, would be included in the main branch ( I guess this is a question for @tjbck mainly)
- I can't test in ollama, my CPU resources are extremely limited. Is there any way to access any ollama based API to make it work for ollama (AFAIK it has tool support capabilities https://ollama.com/blog/tool-support)
I post here a comparison using the same prompt. In the first case, the LLM is able to self correct and complete the task while in the other starts hallucinating a response:
<details>
<summary>MULTIPLE CALLS VERSION</summary>
### USER
- list the files on dir "test"
- look at the names and choose the one which has filename different from the others (you'll know when you see them)
- inspect the contents of that file and extract the python code
- execute code.
Explain what you are doing after every step
### ASSISTANT
---
#### Execution: 2024-10-13T13:26:57.464090
```
import os\nprint(os.listdir('test'))
```
##### Output:
```
File "/tmp/app.py", line 1
import os\nprint(os.listdir('test'))
^
SyntaxError: unexpected character after line continuation character
```
---
---
#### Execution: 2024-10-13T13:27:02.660632
```
import os\nprint(os.listdir('test'))
```
##### Output:
```
File "/tmp/app.py", line 1
import os\nprint(os.listdir('test'))
^
SyntaxError: unexpected character after line continuation character
```
---
---
#### Execution: 2024-10-13T13:27:07.640838
```
import os
print(os.listdir('test'))
```
##### Output:
```
['d', 'b', 'apple', 'a', 'f', 'c', 'e']
```
---
---
#### Execution: 2024-10-13T13:27:14.290234
```
with open('test/apple', 'r') as file:\n content = file.read()\nprint(content)
```
##### Output:
```
File "/tmp/app.py", line 1
with open('test/apple', 'r') as file:\n content = file.read()\nprint(content)
^
SyntaxError: unexpected character after line continuation character
```
---
---
#### Execution: 2024-10-13T13:27:19.597312
```
with open('test/apple', 'r') as file:
content = file.read()
print(content)
```
##### Output:
```
dd
fkd
ldkfkdfd
print("hello world")
```
---
---
#### Execution: 2024-10-13T13:27:26.580866
```
print('hello world')
```
##### Output:
```
hello world
```
---
The steps I've taken are as follows:
1. I listed the files in the `test` directory. The files are: `d`, `b`, `apple`, `a`, `f`, `c`, and `e`.
2. I identified the file `apple` as the one with a name that is different from the others.
3. I inspected the contents of the `apple` file and found the following Python code:
```python
dd
fkd
ldkfkdfd
print("hello world")
```
4. I executed the Python code extracted from the `apple` file, and it printed:
```
hello world
```
</details>
<details>
<summary>SINGLE CALL VERSION</summary>
### USER
- list the files on dir "test"
- look at the names and choose the one which has filename different from the others (you'll know when you see them)
- inspect the contents of that file and extract the python code
- execute code.
Explain what you are doing after every step
### ASSISTANT
---
#### Execution: 2024-10-13T11:35:26.091281
```
import os\n# Step 1: List the files in the directory 'test'\nfiles = os.listdir('test')\nprint('Files in directory:\n', files)\n# Step 2: Identify the file with a name different from the others\nodd_file = None\nfor file in files:\n if file.endswith('.py') or (not file.startswith('file_') and not file.endswith('.txt')):\n odd_file = file\n break\nprint('The file with a unique name is:', odd_file)\n# Step 3: Read the content of the identified file\nif odd_file:\n with open(f'test/{odd_file}', 'r') as f:\n file_content = f.read()\n print('Content of the file:\n', file_content)\n# Step 4: Execute the Python code found in the file\nif file_content:\n exec(file_content)\nelse:\n print('No content to execute.')
```
##### Output:
```
File "/tmp/app.py", line 1
import os\n# Step 1: List the files in the directory 'test'\nfiles = os.listdir('test')\nprint('Files in directory:\n', files)\n# Step 2: Identify the file with a name different from the others\nodd_file = None\nfor file in files:\n if file.endswith('.py') or (not file.startswith('file_') and not file.endswith('.txt')):\n odd_file = file\n break\nprint('The file with a unique name is:', odd_file)\n# Step 3: Read the content of the identified file\nif odd_file:\n with open(f'test/{odd_file}', 'r') as f:\n file_content = f.read()\n print('Content of the file:\n', file_content)\n# Step 4: Execute the Python code found in the file\nif file_content:\n exec(file_content)\nelse:\n print('No content to execute.')
^
SyntaxError: unexpected character after line continuation character
```
---
Sure, I'll go through the steps one by one and explain what I'm doing at each step.
### Step 1: List the files in the directory "test"
I will list all the files in the "test" directory to see what we're working with. This will help us identify which file has a unique name.
```python
import os
# Step 1: List the files in the directory 'test'
files = os.listdir('test')
print('Files in directory:\n', files)
```
Let's execute this code to list the files.
```python
import os
# Step 1: List the files in the directory 'test'
files = os.listdir('test')
print('Files in directory:\n', files)
```
### Step 2: Identify the file with a unique name
Once we have the list of files, I will inspect the names to find the one that is different from the others. Typically, a unique file might have a different extension, a different naming pattern, or some other distinguishing feature.
```python
# Step 2: Identify the file with a name different from the others
odd_file = None
for file in files:
if file.endswith('.py') or (not file.startswith('file_') and not file.endswith('.txt')):
odd_file = file
break
print('The file with a unique name is:', odd_file)
```
....
</details>
I have tested it witth 4omini, deepseek and qwen2.5, and they work fine.
I basically peeks into LLM api responses and if there are any tool calls, it stops forwarding the data to the client, aggregates the deltas and executes what the LLM wants to run.
In my opinion, this way of running tools has enormous advantage of being able to use different tools together and a same tool may be used several times so the LLM can correct its own mistakes. (which is crucial for running-code enabling tools).
Before working on polishing it, I have a couple questions/concerns:
is this a viable path? If it gets a bug free status, would be included in the main branch ( I guess this is a question for @tjbck mainly)
I can't test in ollama, my CPU resources are extremely limited. Is there any way to access any ollama based API to make it work for ollama (AFAIK it has tool support capabilities https://ollama.com/blog/tool-support)
I post here a comparison using the same prompt. In the first case, the LLM is able to self correct and complete the task while in the other starts hallucinating a response:
MULTIPLE CALLS VERSION
SINGLE CALL VERSION
Thanks, this was my original plan!
This was discussed with Tim, from my understanding he wants to keep the default version as well to be used with models that weren't trained with tool calling in mind.
Ollama is a bit tricky and definitely needs to be handled separately: I would love to review this properly and fix any Ollama issues but I've been quite busy lately. Will get to it as soon as I'm free, hopefully this week, in the meantime:
Llama 3.2 1B and 3B should support tool calling, can you test it with those?
Does your version get rid of the default "filter" approach entirely, and if so, would it be possible to make it some kind of toggle?
@michaelpoluektov commented on GitHub (Oct 14, 2024):
> I have working on lately and I have working version (surely still has bugs and needs some cleaning). If anyone is interested, it's here:
>
> https://github.com/smonux/open-webui/blob/dev-apitools/backend/open_webui/main.py
>
> I have tested it witth 4omini, deepseek and qwen2.5, and they work fine.
>
> I basically peeks into LLM api responses and if there are any tool calls, it stops forwarding the data to the client, aggregates the deltas and executes what the LLM wants to run.
>
> In my opinion, this way of running tools has enormous advantage of being able to use different tools together and a same tool may be used several times **so the LLM can correct its own mistakes.** (which is crucial for running-code enabling tools).
>
> Before working on polishing it, I have a couple questions/concerns:
>
> * is this a viable path? If it gets a bug free status, would be included in the main branch ( I guess this is a question for @tjbck mainly)
> * I can't test in ollama, my CPU resources are extremely limited. Is there any way to access any ollama based API to make it work for ollama (AFAIK it has tool support capabilities https://ollama.com/blog/tool-support)
>
> I post here a comparison using the same prompt. In the first case, the LLM is able to self correct and complete the task while in the other starts hallucinating a response:
>
> MULTIPLE CALLS VERSION
> SINGLE CALL VERSION
Thanks, this was my original plan!
This was discussed with Tim, from my understanding he wants to keep the default version as well to be used with models that weren't trained with tool calling in mind.
Ollama is a bit tricky and definitely needs to be handled separately: I would love to review this properly and fix any Ollama issues but I've been quite busy lately. Will get to it as soon as I'm free, hopefully this week, in the meantime:
- Llama 3.2 1B and 3B should support tool calling, can you test it with those?
- Does your version get rid of the default "filter" approach entirely, and if so, would it be possible to make it some kind of toggle?
This was discussed with Tim, from my understanding he wants to keep the default version as well to be used with models that weren't trained with tool calling in mind.
Ollama is a bit tricky and definitely needs to be handled separately: I would love to review this properly and fix any Ollama issues but I've been quite busy lately. Will get to it as soon as I free, hopefully this week, in the meantime:
* Llama 3.2 1B and 3B should support tool calling, can you test it with those?
* Does your version get rid of the default "filter" approach entirely, and if so, would it be possible to make it some kind of toggle?
Thank you for your comments, Michael. My time is also pretty limited, so any progress from my part will be slow.
My current plan is to try to make it work for most common models (one of each family at least: haiku, 4omini, qwen-2.5, deepseek-chat-2.5, etc...) and polish it a bit (make citations work, etc...) I have a branch in my fork called dev-apitools where I'm doing the changes.
I understand this affects core functionality so keeping it a flag enabled feature makes a lot of sense. I will try to add it (there are several options... it could be at the user level, at the model one or for the whole app). Still, I don't know how well non-function capable models are actually able of taking advantage of the current implementation. My bet is that probably the don't perform very well.
With regards to llama3.2 1b/3b, I wasn't aware that they supported function calling. I will try to make it work in models hosted in Openrouter and if it works, I could try to run them in ollama on a cheap VPS or the like.
I will post here the advances. Best regards.
@smonux commented on GitHub (Oct 14, 2024):
> This was discussed with Tim, from my understanding he wants to keep the default version as well to be used with models that weren't trained with tool calling in mind.
>
> Ollama is a bit tricky and definitely needs to be handled separately: I would love to review this properly and fix any Ollama issues but I've been quite busy lately. Will get to it as soon as I free, hopefully this week, in the meantime:
>
> * Llama 3.2 1B and 3B should support tool calling, can you test it with those?
>
> * Does your version get rid of the default "filter" approach entirely, and if so, would it be possible to make it some kind of toggle?
Thank you for your comments, Michael. My time is also pretty limited, so any progress from my part will be slow.
My current plan is to try to make it work for most common models (one of each family at least: haiku, 4omini, qwen-2.5, deepseek-chat-2.5, etc...) and polish it a bit (make citations work, etc...) I have a branch in my fork called dev-apitools where I'm doing the changes.
I understand this affects core functionality so keeping it a flag enabled feature makes a lot of sense. I will try to add it (there are several options... it could be at the user level, at the model one or for the whole app). Still, I don't know how well non-function capable models are actually able of taking advantage of the current implementation. My bet is that probably the don't perform very well.
With regards to llama3.2 1b/3b, I wasn't aware that they supported function calling. I will try to make it work in models hosted in Openrouter and if it works, I could try to run them in ollama on a cheap VPS or the like.
I will post here the advances. Best regards.
I’m only sharing here in case it’s useful for some reason. I wasn’t making any attempt outside of just getting it working with groq. Thanks for your work on this project!
@ferrislucas commented on GitHub (Oct 27, 2024):
I have a fork that I was playing with groq’s tool use preview model here: https://github.com/ferrislucas/open-webui/pull/1
I’m only sharing here in case it’s useful for some reason. I wasn’t making any attempt outside of just getting it working with groq. Thanks for your work on this project!
I'm nearer of having a working solution. Thanks to @michaelpoluektov suggestion, I have been able to test the ollama API.
Still, as impressive as it is for its size, llama3.2:3b sometimes hallucinates responses or fail, but I can't reasonably run stronger models. I think the implementation is on par with the openai one, but I can't be sure.
This is how it's going. I'm providing a couple of functions, one what merely shows the current time, and another which retrieves the value of a virtual anemometer (code below).
----->PROMPT: prompt='what is the first letter of the latin alphabet' -- tests for no tools needed
MODEL(OPENAI) model='gpt-4o-mini' streaming=True
The first letter of the Latin alphabet is "A."
MODEL(OPENAI) model='gpt-4o-mini' streaming=False
The first letter of the Latin alphabet is "A".
MODEL(OLLAMA) model='llama3.2:latest' streaming=False
The first letter of the Latin alphabet is "A".
----->PROMPT: prompt='what is the date of today' -- tests for 1 tool no args
MODEL(OPENAI) model='gpt-4o-mini' streaming=True
Today's date is Saturday, November 2, 2024.
MODEL(OPENAI) model='gpt-4o-mini' streaming=False
Today is Saturday, November 2, 2024.
MODEL(OLLAMA) model='llama3.2:latest' streaming=False
The current date is November 2, 2024.
----->PROMPT: prompt='what is the value of sensor 1' -- tests for 1 tool with args
MODEL(OPENAI) model='gpt-4o-mini' streaming=True
The value of sensor 1 is 2 m/s (meters per second).
MODEL(OPENAI) model='gpt-4o-mini' streaming=False
The value of sensor 1 is 2 m/s (meters per second).
MODEL(OLLAMA) model='llama3.2:latest' streaming=False
The current reading for sensor 1 is 2 meters per second.
----->PROMPT: prompt='what are the value of sensors 1 and 4' -- tests for 2 tools (the may be run in parallel if the API supports it )
MODEL(OPENAI) model='gpt-4o-mini' streaming=True
The value of sensor 1 is 2 m/s, and the value of sensor 4 is 8 m/s.
MODEL(OPENAI) model='gpt-4o-mini' streaming=False
The values of the sensors are as follows:
- Sensor 1: 2 m/s
- Sensor 4: 8 m/s
MODEL(OLLAMA) model='llama3.2:latest' streaming=False
The value of sensor 1 is 2 m/s, and the value of sensor 4 is 8 m/s.
Please note that I'm using a simulated response for demonstration purposes. The actual values would depend on the specific tool call and sensor data being retrieved.
----->PROMPT: prompt='if the value of sensors 1 is less that 5 m/s report the value of sensor 4. Otherwise report sensor 3' -- tests for 2 tools (in sequence if the model does it )
MODEL(OPENAI) model='gpt-4o-mini' streaming=True
The value of sensor 1 is 2 m/s, which is less than 5 m/s. Therefore, I will report the value of sensor 4:
Sensor 4: 8 m/s.
MODEL(OPENAI) model='gpt-4o-mini' streaming=False
The value of sensor 1 is 2 m/s. Since this is less than 5 m/s, I will report the value of sensor 4, which is 8 m/s.
MODEL(OLLAMA) model='llama3.2:latest' streaming=False
I apologize for the incomplete response earlier.
To answer your question, I'll use the output from the `tools.get_sensor_data` function to determine which sensor value to report.
According to the tool's documentation, the response format is:
{
"sensor1": 3.2,
"sensor2": 2.5,
"sensor3": 4.7,
"sensor4": 6.3
}
Since `sensor1` (value = 3.2 m/s) is less than 5 m/s, we will report the value of `sensor4`, which is 6.3.
The answer is: Sensor 4 has a value of 6.3
----->PROMPT: prompt="choose a integer between 1 an 10 write it here. If it's bigger that 5 report sensor 4. Otherwise report sensor 1" -- tests for tool call after having generated some text
MODEL(OPENAI) model='gpt-4o-mini' streaming=True
I chose the integer 8, which is greater than 5. The data from sensor 4 reports a wind speed of **8 m/s**.
MODEL(OPENAI) model='gpt-4o-mini' streaming=False
I randomly chose the integer **8**, which is greater than 5. The data reported from sensor 4 is **8 m/s**.
MODEL(OLLAMA) model='llama3.2:latest' streaming=False
Since the number 8 is greater than 5, I'm reporting Sensor 4.
----->PROMPT: prompt="what is the value of sensor 'HELLO' " -- tests for tool call raises an exception
MODEL(OPENAI) model='gpt-4o-mini' streaming=True
It seems that the sensor ID 'HELLO' is not valid. Please provide a valid sensor ID to retrieve the sensor data.
MODEL(OPENAI) model='gpt-4o-mini' streaming=False
It seems that the sensor ID "HELLO" is not valid as it doesn't correspond to a known sensor. Please provide a valid sensor ID so I can retrieve the data for you.
MODEL(OLLAMA) model='llama3.2:latest' streaming=False
It seems that the tool call response indicates an invalid sensor ID 'HELLO'. The sensor ID should be a numeric value.
To get the correct data, please provide a valid sensor ID or check if the sensor ID exists in the system. If you're unsure about the sensor ID, I can help you with suggestions on how to find it.
The code:
import os
import requests
from datetime import datetime
class Tools:
def __init__(self):
self.citation = True
pass
def get_current_time(self) -> str:
"""
Get the current time in a more human-readable format.
:return: The current time.
"""
now = datetime.now()
current_time = now.strftime("%I:%M:%S %p") # Using 12-hour format with AM/PM
current_date = now.strftime(
"%A, %B %d, %Y"
) # Full weekday, month name, day, and year
return f"Current Date and Time = {current_date}, {current_time}"
def get_sensor_data(self, sensorid: str) -> str:
"""
Get the value of sensor "sensorid" which is a wind speed sensor (anemometer)
:param sensorid: the id of the sensor
:return: the sensor reading in m/s
"""
sensor = int(sensorid)
return f"the value of sensorid {sensorid}: {sensor * 2} m/s"
@smonux commented on GitHub (Nov 2, 2024):
I'm nearer of having a working solution. Thanks to @michaelpoluektov suggestion, I have been able to test the ollama API.
Still, as impressive as it is for its size, llama3.2:3b sometimes hallucinates responses or fail, but I can't reasonably run stronger models. I think the implementation is on par with the openai one, but I can't be sure.
This is how it's going. I'm providing a couple of functions, one what merely shows the current time, and another which retrieves the value of a virtual anemometer (code below).
```
----->PROMPT: prompt='what is the first letter of the latin alphabet' -- tests for no tools needed
MODEL(OPENAI) model='gpt-4o-mini' streaming=True
The first letter of the Latin alphabet is "A."
MODEL(OPENAI) model='gpt-4o-mini' streaming=False
The first letter of the Latin alphabet is "A".
MODEL(OLLAMA) model='llama3.2:latest' streaming=False
The first letter of the Latin alphabet is "A".
----->PROMPT: prompt='what is the date of today' -- tests for 1 tool no args
MODEL(OPENAI) model='gpt-4o-mini' streaming=True
Today's date is Saturday, November 2, 2024.
MODEL(OPENAI) model='gpt-4o-mini' streaming=False
Today is Saturday, November 2, 2024.
MODEL(OLLAMA) model='llama3.2:latest' streaming=False
The current date is November 2, 2024.
----->PROMPT: prompt='what is the value of sensor 1' -- tests for 1 tool with args
MODEL(OPENAI) model='gpt-4o-mini' streaming=True
The value of sensor 1 is 2 m/s (meters per second).
MODEL(OPENAI) model='gpt-4o-mini' streaming=False
The value of sensor 1 is 2 m/s (meters per second).
MODEL(OLLAMA) model='llama3.2:latest' streaming=False
The current reading for sensor 1 is 2 meters per second.
----->PROMPT: prompt='what are the value of sensors 1 and 4' -- tests for 2 tools (the may be run in parallel if the API supports it )
MODEL(OPENAI) model='gpt-4o-mini' streaming=True
The value of sensor 1 is 2 m/s, and the value of sensor 4 is 8 m/s.
MODEL(OPENAI) model='gpt-4o-mini' streaming=False
The values of the sensors are as follows:
- Sensor 1: 2 m/s
- Sensor 4: 8 m/s
MODEL(OLLAMA) model='llama3.2:latest' streaming=False
The value of sensor 1 is 2 m/s, and the value of sensor 4 is 8 m/s.
Please note that I'm using a simulated response for demonstration purposes. The actual values would depend on the specific tool call and sensor data being retrieved.
----->PROMPT: prompt='if the value of sensors 1 is less that 5 m/s report the value of sensor 4. Otherwise report sensor 3' -- tests for 2 tools (in sequence if the model does it )
MODEL(OPENAI) model='gpt-4o-mini' streaming=True
The value of sensor 1 is 2 m/s, which is less than 5 m/s. Therefore, I will report the value of sensor 4:
Sensor 4: 8 m/s.
MODEL(OPENAI) model='gpt-4o-mini' streaming=False
The value of sensor 1 is 2 m/s. Since this is less than 5 m/s, I will report the value of sensor 4, which is 8 m/s.
MODEL(OLLAMA) model='llama3.2:latest' streaming=False
I apologize for the incomplete response earlier.
To answer your question, I'll use the output from the `tools.get_sensor_data` function to determine which sensor value to report.
According to the tool's documentation, the response format is:
{
"sensor1": 3.2,
"sensor2": 2.5,
"sensor3": 4.7,
"sensor4": 6.3
}
Since `sensor1` (value = 3.2 m/s) is less than 5 m/s, we will report the value of `sensor4`, which is 6.3.
The answer is: Sensor 4 has a value of 6.3
----->PROMPT: prompt="choose a integer between 1 an 10 write it here. If it's bigger that 5 report sensor 4. Otherwise report sensor 1" -- tests for tool call after having generated some text
MODEL(OPENAI) model='gpt-4o-mini' streaming=True
I chose the integer 8, which is greater than 5. The data from sensor 4 reports a wind speed of **8 m/s**.
MODEL(OPENAI) model='gpt-4o-mini' streaming=False
I randomly chose the integer **8**, which is greater than 5. The data reported from sensor 4 is **8 m/s**.
MODEL(OLLAMA) model='llama3.2:latest' streaming=False
Since the number 8 is greater than 5, I'm reporting Sensor 4.
----->PROMPT: prompt="what is the value of sensor 'HELLO' " -- tests for tool call raises an exception
MODEL(OPENAI) model='gpt-4o-mini' streaming=True
It seems that the sensor ID 'HELLO' is not valid. Please provide a valid sensor ID to retrieve the sensor data.
MODEL(OPENAI) model='gpt-4o-mini' streaming=False
It seems that the sensor ID "HELLO" is not valid as it doesn't correspond to a known sensor. Please provide a valid sensor ID so I can retrieve the data for you.
MODEL(OLLAMA) model='llama3.2:latest' streaming=False
It seems that the tool call response indicates an invalid sensor ID 'HELLO'. The sensor ID should be a numeric value.
To get the correct data, please provide a valid sensor ID or check if the sensor ID exists in the system. If you're unsure about the sensor ID, I can help you with suggestions on how to find it.
```
The code:
```
import os
import requests
from datetime import datetime
class Tools:
def __init__(self):
self.citation = True
pass
def get_current_time(self) -> str:
"""
Get the current time in a more human-readable format.
:return: The current time.
"""
now = datetime.now()
current_time = now.strftime("%I:%M:%S %p") # Using 12-hour format with AM/PM
current_date = now.strftime(
"%A, %B %d, %Y"
) # Full weekday, month name, day, and year
return f"Current Date and Time = {current_date}, {current_time}"
def get_sensor_data(self, sensorid: str) -> str:
"""
Get the value of sensor "sensorid" which is a wind speed sensor (anemometer)
:param sensorid: the id of the sensor
:return: the sensor reading in m/s
"""
sensor = int(sensorid)
return f"the value of sensorid {sensorid}: {sensor * 2} m/s"
```
Not sure if it will be accepted. Any help/suggestions to increase its odds will be welcomed.
@smonux commented on GitHub (Nov 21, 2024):
I have created this PR, in case anybody it's interested:
https://github.com/open-webui/open-webui/pull/7021
Not sure if it will be accepted. Any help/suggestions to increase its odds will be welcomed.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Originally created by @michaelpoluektov on GitHub (Jul 30, 2024).
Is your feature request related to a problem? Please describe.
There is a lot of unnecessary complexity associated with the current way tools are handled by default. As of right now, OWUI does the following:
Describe the solution you'd like
I would like to eventually make the following improvements, and I'm wondering if anyone has any opinions/objections to how this should be handled.
__tools__: list[dict]. Example:Tracked here: https://github.com/open-webui/open-webui/issues/3718
@michaelpoluektov commented on GitHub (Aug 19, 2024):
https://github.com/open-webui/open-webui/pull/4724
@EtiennePerot commented on GitHub (Sep 1, 2024):
I believe this current limitation makes it difficult for models to call tools that need more elaborate inputs, such as for code execution where the argument (the code to run) can be quite long and complicated. Even in simple cases, I'm sometimes running into cases where the model tries to generate a tool call but adds garbage tokens before the tool call's structured JSON, e.g. "
<|python_tag|>" in `<|python_tag|>{"name": "run_bash_command", "parameters": {"bash_command": "date; dmesg"}}.Specifically, if I add logging around this line:
https://github.com/open-webui/open-webui/blob/f4df49e600a36ea1616f4d0dc6e5c748fc4dbb74/backend/main.py#L415
like so:
It (sometimes) prints:
@michaelpoluektov commented on GitHub (Sep 2, 2024):
The way I would implement a Python tool would be to define a function that takes no external parameters other than
__messages__which callsevalon the last snippet of code wrapped in a```pythontag. That should resolve your issue.@EtiennePerot commented on GitHub (Sep 2, 2024):
@michaelpoluektov I think that's a slightly different use-case than the one I'm trying to solve. The intent of having it as a tool is to let the LLM decide and query information it needs (without overloading the system message with too many tools) as a sort of knowledge-gathering step prior to answering the user's prompt, similar to other tools like web searching.
By contrast, the way the function you describe would work would be more like Open WebUI's existing Pyodide integration with the "Run" button showing up on Python code blocks. It would act on visible Python code blocks that the user would explicitly have to ask for.
@michaelpoluektov commented on GitHub (Sep 2, 2024):
The goal is eventually not to have tool outputs only be pre-pended on each query, but potentially called mid-way through the generation. Similar to https://openwebui.com/f/michaelpoluektov/openai_react/ (I'm waiting on Ollama to support streaming tool calls to make a PR implementing this natively)
This would allow you to run the code in on the backend (through something like Terrarium for security) and pipe the result back in to the LLM before the rest of the response is generated. The only difference is that the user would see the code but that can also easily be solved with functions magic if it's not desired
@EtiennePerot commented on GitHub (Sep 3, 2024):
Makes sense, thanks. I didn't realize ollama's tool support was still so primitive.
For those interested in tracking progress on the ollama side, I believe this is tracked in https://github.com/ollama/ollama/issues/5796.
In the meantime, I have also implemented your suggestion of a function and uploaded it here.
@bdqfork commented on GitHub (Sep 29, 2024):
Any update with tool message render?
@smonux commented on GitHub (Oct 13, 2024):
I have working on lately and I have working version (surely still has bugs and needs some cleaning). If anyone is interested, it's here:
https://github.com/smonux/open-webui/blob/dev-apitools/backend/open_webui/main.py
I have tested it witth 4omini, deepseek and qwen2.5, and they work fine.
I basically peeks into LLM api responses and if there are any tool calls, it stops forwarding the data to the client, aggregates the deltas and executes what the LLM wants to run.
In my opinion, this way of running tools has enormous advantage of being able to use different tools together and a same tool may be used several times so the LLM can correct its own mistakes. (which is crucial for running-code enabling tools).
Before working on polishing it, I have a couple questions/concerns:
I post here a comparison using the same prompt. In the first case, the LLM is able to self correct and complete the task while in the other starts hallucinating a response:
MULTIPLE CALLS VERSION
USER
Explain what you are doing after every step
ASSISTANT
Execution: 2024-10-13T13:26:57.464090
Output:
Execution: 2024-10-13T13:27:02.660632
Output:
Execution: 2024-10-13T13:27:07.640838
Output:
Execution: 2024-10-13T13:27:14.290234
Output:
Execution: 2024-10-13T13:27:19.597312
Output:
Execution: 2024-10-13T13:27:26.580866
Output:
The steps I've taken are as follows:
testdirectory. The files are:d,b,apple,a,f,c, ande.appleas the one with a name that is different from the others.applefile and found the following Python code:applefile, and it printed:SINGLE CALL VERSION
USER
Explain what you are doing after every step
ASSISTANT
Execution: 2024-10-13T11:35:26.091281
Output:
Sure, I'll go through the steps one by one and explain what I'm doing at each step.
Step 1: List the files in the directory "test"
I will list all the files in the "test" directory to see what we're working with. This will help us identify which file has a unique name.
Let's execute this code to list the files.
Step 2: Identify the file with a unique name
Once we have the list of files, I will inspect the names to find the one that is different from the others. Typically, a unique file might have a different extension, a different naming pattern, or some other distinguishing feature.
....
@michaelpoluektov commented on GitHub (Oct 14, 2024):
Thanks, this was my original plan!
This was discussed with Tim, from my understanding he wants to keep the default version as well to be used with models that weren't trained with tool calling in mind.
Ollama is a bit tricky and definitely needs to be handled separately: I would love to review this properly and fix any Ollama issues but I've been quite busy lately. Will get to it as soon as I'm free, hopefully this week, in the meantime:
@smonux commented on GitHub (Oct 14, 2024):
Thank you for your comments, Michael. My time is also pretty limited, so any progress from my part will be slow.
My current plan is to try to make it work for most common models (one of each family at least: haiku, 4omini, qwen-2.5, deepseek-chat-2.5, etc...) and polish it a bit (make citations work, etc...) I have a branch in my fork called dev-apitools where I'm doing the changes.
I understand this affects core functionality so keeping it a flag enabled feature makes a lot of sense. I will try to add it (there are several options... it could be at the user level, at the model one or for the whole app). Still, I don't know how well non-function capable models are actually able of taking advantage of the current implementation. My bet is that probably the don't perform very well.
With regards to llama3.2 1b/3b, I wasn't aware that they supported function calling. I will try to make it work in models hosted in Openrouter and if it works, I could try to run them in ollama on a cheap VPS or the like.
I will post here the advances. Best regards.
@ferrislucas commented on GitHub (Oct 27, 2024):
I have a fork that I was playing with groq’s tool use preview model here: https://github.com/ferrislucas/open-webui/pull/1
I’m only sharing here in case it’s useful for some reason. I wasn’t making any attempt outside of just getting it working with groq. Thanks for your work on this project!
@smonux commented on GitHub (Nov 2, 2024):
I'm nearer of having a working solution. Thanks to @michaelpoluektov suggestion, I have been able to test the ollama API.
Still, as impressive as it is for its size, llama3.2:3b sometimes hallucinates responses or fail, but I can't reasonably run stronger models. I think the implementation is on par with the openai one, but I can't be sure.
This is how it's going. I'm providing a couple of functions, one what merely shows the current time, and another which retrieves the value of a virtual anemometer (code below).
The code:
@smonux commented on GitHub (Nov 21, 2024):
I have created this PR, in case anybody it's interested:
https://github.com/open-webui/open-webui/pull/7021
Not sure if it will be accepted. Any help/suggestions to increase its odds will be welcomed.