├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── env.py ├── examples ├── ai.py ├── duckduckgo-agent.py └── smolagents.py ├── main.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | # PyPI configuration file 171 | .pypirc 172 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Open WebUI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # open-webui/bot 2 | 3 | This repository provides an experimental boilerplate for building bots compatible with the **Open WebUI** "Channels" feature (introduced in version 0.5.0). It serves as a proof of concept to demonstrate bot-building capabilities while highlighting the potential of asynchronous communication enabled by Channels. 4 | 5 | ## ⚡ Key Highlights 6 | - **Highly Experimental**: This is an early-stage project showcasing basic bot-building functionality. Expect major API changes in the future. 7 | - **Extensible Framework**: Designed as a foundation for further development, with plans to enhance APIs, developer tooling, and usability. 8 | - **Asynchronous Communication**: Leverages Open WebUI Channels for event-driven workflows. 9 | 10 | ## 🛠️ Getting Started with Examples 11 | This repository includes an `/examples` folder containing runnable example bots that demonstrate basic functionality. 12 | 13 | To run an example, execute the corresponding module using the `-m` flag in Python. For example, to run the `ai` example: 14 | 15 | ```bash 16 | python -m examples.ai 17 | ``` 18 | 19 | > **Note**: Ensure that your current working directory (PWD) is the root of this repository when running examples, as this is required for proper execution. 20 | 21 | Replace `ai` in the command above with the specific example you’d like to execute from the `/examples` folder. 22 | 23 | ## 🚧 Disclaimer 24 | This project is an early-stage proof of concept. **APIs will break** and existing functionality may change as Open WebUI evolves to include native bot support. This repository is not production-ready and primarily serves experimental and exploratory purposes. 25 | 26 | ## 🎯 Future Vision 27 | We aim to introduce improved APIs, enhanced developer tooling, and seamless native support for bots directly within Open WebUI. The ultimate goal is to make building bots easier, faster, and more intuitive. 28 | 29 | --- 30 | Contributions, feedback, and experimentation are encouraged. Join us in shaping the future of bot-building on Open WebUI! -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-webui/bot/5bba99c699e86bd54c315610e4424efe209adae1/__init__.py -------------------------------------------------------------------------------- /env.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | try: 4 | from dotenv import load_dotenv 5 | 6 | load_dotenv() 7 | except ImportError: 8 | print("dotenv not installed, skipping...") 9 | 10 | 11 | WEBUI_URL = os.getenv("WEBUI_URL", "http://localhost:8080") 12 | TOKEN = os.getenv("TOKEN", "") 13 | -------------------------------------------------------------------------------- /examples/ai.py: -------------------------------------------------------------------------------- 1 | # WARNING: This might not work in the future. Do NOT use this in production. 2 | 3 | import asyncio 4 | import socketio 5 | from env import WEBUI_URL, TOKEN 6 | from utils import send_message, send_typing 7 | 8 | 9 | MODEL_ID = "llama3.2:latest" 10 | 11 | # Create an asynchronous Socket.IO client instance 12 | sio = socketio.AsyncClient(logger=False, engineio_logger=False) 13 | 14 | 15 | # Event handlers 16 | @sio.event 17 | async def connect(): 18 | print("Connected!") 19 | 20 | 21 | @sio.event 22 | async def disconnect(): 23 | print("Disconnected from the server!") 24 | 25 | 26 | import aiohttp 27 | import asyncio 28 | 29 | 30 | async def openai_chat_completion(messages): 31 | payload = { 32 | "model": MODEL_ID, 33 | "messages": messages, 34 | "stream": False, 35 | } 36 | 37 | async with aiohttp.ClientSession() as session: 38 | async with session.post( 39 | f"{WEBUI_URL}/api/chat/completions", 40 | headers={"Authorization": f"Bearer {TOKEN}"}, 41 | json=payload, 42 | ) as response: 43 | if response.status == 200: 44 | return await response.json() 45 | else: 46 | # Optional: Handle errors or return raw response text 47 | return {"error": await response.text(), "status": response.status} 48 | 49 | 50 | # Define a function to handle channel events 51 | def events(user_id): 52 | @sio.on("channel-events") 53 | async def channel_events(data): 54 | if data["user"]["id"] == user_id: 55 | # Ignore events from the bot itself 56 | return 57 | 58 | if data["data"]["type"] == "message": 59 | print(f'{data["user"]["name"]}: {data["data"]["data"]["content"]}') 60 | await send_typing(sio, data["channel_id"]) 61 | 62 | async def send_typing_until_complete(channel_id, coro): 63 | """ 64 | Sends typing indicators every second until the provided coroutine completes. 65 | """ 66 | task = asyncio.create_task(coro) # Begin the provided coroutine task 67 | try: 68 | # While the task is running, send typing indicators every second 69 | while not task.done(): 70 | await send_typing(sio, channel_id) 71 | await asyncio.sleep(1) 72 | # Await the actual result of the coroutine 73 | return await task 74 | except Exception as e: 75 | task.cancel() 76 | raise e # Propagate any exceptions that occurred in the coroutine 77 | 78 | # OpenAI API coroutine 79 | # This uses naive implementation of OpenAI API, that does not utilize the context of the conversation 80 | openai_task = openai_chat_completion( 81 | [ 82 | {"role": "system", "content": "You are a friendly AI."}, 83 | {"role": "user", "content": data["data"]["data"]["content"]}, 84 | ] 85 | ) 86 | 87 | try: 88 | # Run OpenAI coroutine while showing typing indicators 89 | response = await send_typing_until_complete( 90 | data["channel_id"], openai_task 91 | ) 92 | 93 | if response.get("choices"): 94 | completion = response["choices"][0]["message"]["content"] 95 | await send_message(data["channel_id"], completion) 96 | else: 97 | await send_message( 98 | data["channel_id"], "I'm sorry, I don't understand." 99 | ) 100 | except Exception: 101 | await send_message( 102 | data["channel_id"], 103 | "Something went wrong while processing your request.", 104 | ) 105 | 106 | 107 | # Define an async function for the main workflow 108 | async def main(): 109 | try: 110 | print(f"Connecting to {WEBUI_URL}...") 111 | await sio.connect( 112 | WEBUI_URL, socketio_path="/ws/socket.io", transports=["websocket"] 113 | ) 114 | print("Connection established!") 115 | except Exception as e: 116 | print(f"Failed to connect: {e}") 117 | return 118 | 119 | # Callback function for user-join 120 | async def join_callback(data): 121 | events(data["id"]) # Attach the event handlers dynamically 122 | 123 | # Authenticate with the server 124 | await sio.emit("user-join", {"auth": {"token": TOKEN}}, callback=join_callback) 125 | 126 | # Wait indefinitely to keep the connection open 127 | await sio.wait() 128 | 129 | 130 | # Actually run the async `main` function using `asyncio` 131 | if __name__ == "__main__": 132 | asyncio.run(main()) 133 | -------------------------------------------------------------------------------- /examples/duckduckgo-agent.py: -------------------------------------------------------------------------------- 1 | # WARNING: This might not work in the future. Do NOT use this in production. 2 | 3 | import asyncio 4 | import socketio 5 | from smolagents import ToolCallingAgent, LiteLLMModel, DuckDuckGoSearchTool 6 | 7 | 8 | from env import WEBUI_URL, TOKEN 9 | from utils import send_message, send_typing 10 | 11 | search_tool = DuckDuckGoSearchTool() 12 | 13 | MODEL_ID = "gpt-4o" 14 | 15 | model = LiteLLMModel( 16 | model_id=f"openai/{MODEL_ID}", api_base=f"{WEBUI_URL}/api/", api_key=TOKEN 17 | ) 18 | agent = ToolCallingAgent(tools=[search_tool], model=model) 19 | 20 | # Create an asynchronous Socket.IO client instance 21 | sio = socketio.AsyncClient(logger=False, engineio_logger=False) 22 | 23 | 24 | # Event handlers 25 | @sio.event 26 | async def connect(): 27 | print("Connected!") 28 | 29 | 30 | @sio.event 31 | async def disconnect(): 32 | print("Disconnected from the server!") 33 | 34 | 35 | # Define a function to handle channel events 36 | def events(user_id): 37 | @sio.on("channel-events") 38 | async def channel_events(data): 39 | if data["user"]["id"] == user_id: 40 | # Ignore events from the bot itself 41 | return 42 | 43 | if data["data"]["type"] == "message": 44 | print(f'{data["user"]["name"]}: {data["data"]["data"]["content"]}') 45 | 46 | # Send typing events every second while processing the input 47 | async def simulate_typing(channel_id): 48 | try: 49 | while not processing_event.is_set(): 50 | await send_typing(sio, channel_id) 51 | await asyncio.sleep(1) 52 | except asyncio.CancelledError: 53 | pass 54 | 55 | # Create an asyncio.Event to manage typing simulation 56 | processing_event = asyncio.Event() 57 | typing_task = asyncio.create_task(simulate_typing(data["channel_id"])) 58 | 59 | try: 60 | # Run the blocking agent.run in a non-blocking way using asyncio 61 | loop = asyncio.get_running_loop() 62 | output = await loop.run_in_executor( 63 | None, agent.run, data["data"]["data"]["content"] 64 | ) 65 | finally: 66 | # Signal that typing simulation should stop 67 | processing_event.set() 68 | # Wait for the typing task to finish 69 | await typing_task 70 | 71 | # Send the generated output as a message 72 | await send_message(data["channel_id"], f"{output}") 73 | 74 | 75 | # Define an async function for the main workflow 76 | async def main(): 77 | try: 78 | print(f"Connecting to {WEBUI_URL}...") 79 | await sio.connect( 80 | WEBUI_URL, socketio_path="/ws/socket.io", transports=["websocket"] 81 | ) 82 | print("Connection established!") 83 | except Exception as e: 84 | print(f"Failed to connect: {e}") 85 | return 86 | 87 | # Callback function for user-join 88 | async def join_callback(data): 89 | events(data["id"]) # Attach the event handlers dynamically 90 | 91 | # Authenticate with the server 92 | await sio.emit("user-join", {"auth": {"token": TOKEN}}, callback=join_callback) 93 | 94 | # Wait indefinitely to keep the connection open 95 | await sio.wait() 96 | 97 | 98 | # Actually run the async `main` function using `asyncio` 99 | if __name__ == "__main__": 100 | asyncio.run(main()) 101 | -------------------------------------------------------------------------------- /examples/smolagents.py: -------------------------------------------------------------------------------- 1 | # WARNING: This might not work in the future. Do NOT use this in production. 2 | 3 | import asyncio 4 | import socketio 5 | from smolagents import CodeAgent, LiteLLMModel, DuckDuckGoSearchTool 6 | 7 | 8 | from env import WEBUI_URL, TOKEN 9 | from utils import send_message, send_typing 10 | 11 | # search_tool = DuckDuckGoSearchTool() 12 | 13 | MODEL_ID = "llama3.2:latest" 14 | 15 | model = LiteLLMModel( 16 | model_id=f"openai/{MODEL_ID}", api_base=f"{WEBUI_URL}/api/", api_key=TOKEN 17 | ) 18 | agent = CodeAgent( 19 | tools=[], model=model, additional_authorized_imports=["requests", "bs4"] 20 | ) 21 | 22 | 23 | # Create an asynchronous Socket.IO client instance 24 | sio = socketio.AsyncClient(logger=False, engineio_logger=False) 25 | 26 | 27 | # Event handlers 28 | @sio.event 29 | async def connect(): 30 | print("Connected!") 31 | 32 | 33 | @sio.event 34 | async def disconnect(): 35 | print("Disconnected from the server!") 36 | 37 | 38 | # Define a function to handle channel events 39 | def events(user_id): 40 | @sio.on("channel-events") 41 | async def channel_events(data): 42 | if data["user"]["id"] == user_id: 43 | # Ignore events from the bot itself 44 | return 45 | 46 | if data["data"]["type"] == "message": 47 | print(f'{data["user"]["name"]}: {data["data"]["data"]["content"]}') 48 | 49 | # Send typing events every second while processing the input 50 | async def simulate_typing(channel_id): 51 | try: 52 | while not processing_event.is_set(): 53 | await send_typing(sio, channel_id) 54 | await asyncio.sleep(1) 55 | except asyncio.CancelledError: 56 | pass 57 | 58 | # Create an asyncio.Event to manage typing simulation 59 | processing_event = asyncio.Event() 60 | typing_task = asyncio.create_task(simulate_typing(data["channel_id"])) 61 | 62 | try: 63 | # Run the blocking agent.run in a non-blocking way using asyncio 64 | loop = asyncio.get_running_loop() 65 | output = await loop.run_in_executor( 66 | None, agent.run, data["data"]["data"]["content"] 67 | ) 68 | finally: 69 | # Signal that typing simulation should stop 70 | processing_event.set() 71 | # Wait for the typing task to finish 72 | await typing_task 73 | 74 | # Send the generated output as a message 75 | await send_message(data["channel_id"], f"{output}") 76 | 77 | 78 | # Define an async function for the main workflow 79 | async def main(): 80 | try: 81 | print(f"Connecting to {WEBUI_URL}...") 82 | await sio.connect( 83 | WEBUI_URL, socketio_path="/ws/socket.io", transports=["websocket"] 84 | ) 85 | print("Connection established!") 86 | except Exception as e: 87 | print(f"Failed to connect: {e}") 88 | return 89 | 90 | # Callback function for user-join 91 | async def join_callback(data): 92 | events(data["id"]) # Attach the event handlers dynamically 93 | 94 | # Authenticate with the server 95 | await sio.emit("user-join", {"auth": {"token": TOKEN}}, callback=join_callback) 96 | 97 | # Wait indefinitely to keep the connection open 98 | await sio.wait() 99 | 100 | 101 | # Actually run the async `main` function using `asyncio` 102 | if __name__ == "__main__": 103 | asyncio.run(main()) 104 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import socketio 3 | from env import WEBUI_URL, TOKEN 4 | from utils import send_message, send_typing 5 | 6 | # Create an asynchronous Socket.IO client instance 7 | sio = socketio.AsyncClient(logger=False, engineio_logger=False) 8 | 9 | 10 | # Event handlers 11 | @sio.event 12 | async def connect(): 13 | print("Connected!") 14 | 15 | 16 | @sio.event 17 | async def disconnect(): 18 | print("Disconnected from the server!") 19 | 20 | 21 | # Define a function to handle channel events 22 | def events(user_id): 23 | @sio.on("channel-events") 24 | async def channel_events(data): 25 | if data["user"]["id"] == user_id: 26 | # Ignore events from the bot itself 27 | return 28 | 29 | if data["data"]["type"] == "message": 30 | print(f'{data["user"]["name"]}: {data["data"]["data"]["content"]}') 31 | await send_typing(sio, data["channel_id"]) 32 | await asyncio.sleep(1) # Simulate a delay 33 | await send_message(data["channel_id"], "Pong!") 34 | 35 | 36 | # Define an async function for the main workflow 37 | async def main(): 38 | try: 39 | print(f"Connecting to {WEBUI_URL}...") 40 | await sio.connect( 41 | WEBUI_URL, socketio_path="/ws/socket.io", transports=["websocket"] 42 | ) 43 | print("Connection established!") 44 | except Exception as e: 45 | print(f"Failed to connect: {e}") 46 | return 47 | 48 | # Callback function for user-join 49 | async def join_callback(data): 50 | events(data["id"]) # Attach the event handlers dynamically 51 | 52 | # Authenticate with the server 53 | await sio.emit("user-join", {"auth": {"token": TOKEN}}, callback=join_callback) 54 | 55 | # Wait indefinitely to keep the connection open 56 | await sio.wait() 57 | 58 | 59 | # Actually run the async `main` function using `asyncio` 60 | if __name__ == "__main__": 61 | asyncio.run(main()) 62 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import socketio 3 | from env import WEBUI_URL, TOKEN 4 | 5 | 6 | async def send_message(channel_id: str, message: str): 7 | url = f"{WEBUI_URL}/api/v1/channels/{channel_id}/messages/post" 8 | headers = {"Authorization": f"Bearer {TOKEN}"} 9 | data = {"content": str(message)} 10 | 11 | async with aiohttp.ClientSession() as session: 12 | async with session.post(url, headers=headers, json=data) as response: 13 | if response.status != 200: 14 | # Raise an exception if the request fails 15 | raise aiohttp.ClientResponseError( 16 | request_info=response.request_info, 17 | history=response.history, 18 | status=response.status, 19 | message=await response.text(), 20 | headers=response.headers, 21 | ) 22 | # Return response JSON if successful 23 | return await response.json() 24 | 25 | 26 | async def send_typing(sio: socketio.AsyncClient, channel_id: str): 27 | await sio.emit( 28 | "channel-events", 29 | { 30 | "channel_id": channel_id, 31 | "data": {"type": "typing", "data": {"typing": True}}, 32 | }, 33 | ) 34 | --------------------------------------------------------------------------------