From 957f2cecaf34c058ff4699bee1ca6c0a82b1f676 Mon Sep 17 00:00:00 2001 From: DIEHARDERS Date: Thu, 28 Mar 2024 14:46:29 -0700 Subject: [PATCH 1/6] HBAI-209 Changed scripts to add custom build args. Read in runtime args. Add a tkinter window GUI. --- backends/main.py | 65 ++++++++++++++-- package.json | 7 +- py-to-exe-config.debug.json | 77 ------------------ py-to-exe-config.dev.json | 73 ++++++++++++++++++ py-to-exe-config.prod.json | 150 ++++++++++++++++++------------------ 5 files changed, 209 insertions(+), 163 deletions(-) delete mode 100644 py-to-exe-config.debug.json create mode 100644 py-to-exe-config.dev.json diff --git a/backends/main.py b/backends/main.py index e4040fa..5cf5363 100644 --- a/backends/main.py +++ b/backends/main.py @@ -1,5 +1,7 @@ -# import sys import os +import sys +import signal +import threading import glob import json import uvicorn @@ -18,6 +20,8 @@ ) from fastapi.middleware.cors import CORSMiddleware from sse_starlette.sse import EventSourceResponse +import tkinter as tk +from PIL import Image, ImageTk from contextlib import asynccontextmanager from inference import text_llama_index from embedding import embedding @@ -32,10 +36,6 @@ HfApi, ) -# Remove prints in prod when deploying in window mode. -# sys.stdout = open(os.devnull, "w") -# sys.stderr = open(os.devnull, "w") - VECTOR_DB_FOLDER = "chromadb" MEMORY_FOLDER = "memories" PARSED_FOLDER = "parsed" @@ -48,6 +48,28 @@ BOT_SETTINGS_FILE_NAME = "bots.json" SERVER_PORT = 8008 + +# Parse runtime arguments passed to script +def parse_runtime_args(): + # Command-line arguments are accessed via sys.argv + arguments = sys.argv[1:] + # Initialize variables to store parsed arguments + mode = None + # Iterate through arguments and parse them + for arg in arguments: + if arg.startswith("--mode="): + mode = arg.split("=")[1] + return mode + + +buildEnv = parse_runtime_args() +isDev = buildEnv == "dev" +isProd = buildEnv == "prod" +if isProd: + # Remove prints in prod when deploying in window mode + sys.stdout = open(os.devnull, "w") + sys.stderr = open(os.devnull, "w") + # Path to the .env file in the parent directory current_directory = os.path.dirname(os.path.abspath(__file__)) parent_directory = os.path.dirname(current_directory) @@ -1234,7 +1256,27 @@ def get_bot_settings() -> classes.BotSettingsResponse: # Methods... -def start_homebrew_server(): +class Window: + def __init__(self, master): + # @TODO Swap out with a UI or other image + self.img = Image.open("public/splash.png") + self.img = self.img.resize((320, 300), Image.FILTERED) + + self.img = ImageTk.PhotoImage(self.img) + + label = tk.Label(master, image=self.img) + label.pack(expand=True, fill=tk.BOTH) + + +# Function to create and run the Tkinter window +def run_GUI(): + root = tk.Tk() + root.title("OpenBrew Server") + window = Window(root) + root.mainloop() + + +def run_server(): try: print(f"{common.PRNT_API} Starting API server...") # Start the ASGI server @@ -1251,5 +1293,12 @@ def start_homebrew_server(): if __name__ == "__main__": - # Starts the homebrew API server - start_homebrew_server() + # Start the API server in a separate thread + fastapi_thread = threading.Thread(target=run_server) + fastapi_thread.start() + # GUI window + if isProd: + run_GUI() + # Handle stopping the server when window is closed + print(f"{common.PRNT_API} Shutting down on user action", flush=True) + os.kill(os.getpid(), signal.SIGINT) diff --git a/package.json b/package.json index c55d6ab..3db598f 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,11 @@ "scripts": { "clean": "rimraf includes renderer/.next renderer/out build release main.spec", "dev": "next dev", - "server": "python ./backends/main.py", + "server:dev": "python ./backends/main.py --mode=dev", + "server:prod": "python ./backends/main.py --mode=prod", "build": "yarn run build:api && next build renderer", - "build:api:prod": "yarn run python-deps && pyinstaller --noconfirm --onedir --windowed --icon C:/Project Files/brain-dump-ai/backend-homebrew-ai/public/favicon.ico --name OpenBrew-Server --contents-directory _deps --clean --add-data C:/Python311/Lib/site-packages/llama_index/VERSION;./llama_index --add-data C:/Python311/Lib/site-packages/tiktoken_ext;tiktoken_ext/ C:/Project Files/brain-dump-ai/backend-homebrew-ai/backends/main.py", - "build:api:debug": "yarn run python-deps && pyinstaller --noconfirm --onedir --console --icon C:/Project Files/brain-dump-ai/backend-homebrew-ai/public/favicon.ico --name OpenBrew-Server --contents-directory _deps --clean --debug imports --add-data C:/Python311/Lib/site-packages/llama_index/VERSION;./llama_index C:/Project Files/brain-dump-ai/backend-homebrew-ai/backends/main.py", + "build:api:dev": "yarn run python-deps && pyinstaller --noconfirm --onedir --console --icon C:/Project Files/brain-dump-ai/backend-homebrew-ai/public/favicon.ico --name OpenBrew-Server --contents-directory _deps --clean --debug imports --add-data C:/Python311/Lib/site-packages/llama_index/VERSION;./llama_index C:/Project Files/brain-dump-ai/backend-homebrew-ai/backends/main.py --mode=dev", + "build:api:prod": "yarn run python-deps && pyinstaller --noconfirm --onedir --windowed --icon C:/Project Files/brain-dump-ai/backend-homebrew-ai/public/favicon.ico --name OpenBrew-Server --contents-directory _deps --clean --add-data C:/Python311/Lib/site-packages/llama_index/VERSION;./llama_index --add-data C:/Python311/Lib/site-packages/tiktoken_ext;tiktoken_ext/ C:/Project Files/brain-dump-ai/backend-homebrew-ai/backends/main.py --mode=prod", "python-deps": "pip install -r requirements.txt", "release": "yarn run build && electron-builder", "release:win": "yarn run build && electron-builder --win --x64", diff --git a/py-to-exe-config.debug.json b/py-to-exe-config.debug.json deleted file mode 100644 index 280dd94..0000000 --- a/py-to-exe-config.debug.json +++ /dev/null @@ -1,77 +0,0 @@ -{ - "version": "auto-py-to-exe-configuration_v1", - "pyinstallerOptions": [ - { - "optionDest": "noconfirm", - "value": true - }, - { - "optionDest": "filenames", - "value": "C:/Project Files/brain-dump-ai/backend-homebrew-ai/backends/main.py" - }, - { - "optionDest": "onefile", - "value": false - }, - { - "optionDest": "console", - "value": true - }, - { - "optionDest": "icon_file", - "value": "C:/Project Files/brain-dump-ai/backend-homebrew-ai/public/favicon.ico" - }, - { - "optionDest": "name", - "value": "OpenBrew-Server" - }, - { - "optionDest": "contents_directory", - "value": "_deps" - }, - { - "optionDest": "clean_build", - "value": true - }, - { - "optionDest": "debug", - "value": "imports" - }, - { - "optionDest": "strip", - "value": false - }, - { - "optionDest": "noupx", - "value": false - }, - { - "optionDest": "disable_windowed_traceback", - "value": false - }, - { - "optionDest": "uac_admin", - "value": false - }, - { - "optionDest": "uac_uiaccess", - "value": false - }, - { - "optionDest": "argv_emulation", - "value": false - }, - { - "optionDest": "bootloader_ignore_signals", - "value": false - }, - { - "optionDest": "datas", - "value": "C:/Python311/Lib/site-packages/llama_index/VERSION;./llama_index" - } - ], - "nonPyinstallerOptions": { - "increaseRecursionLimit": true, - "manualArguments": "" - } -} diff --git a/py-to-exe-config.dev.json b/py-to-exe-config.dev.json new file mode 100644 index 0000000..5ff6c28 --- /dev/null +++ b/py-to-exe-config.dev.json @@ -0,0 +1,73 @@ +{ + "version": "auto-py-to-exe-configuration_v1", + "pyinstallerOptions": [ + { + "optionDest": "noconfirm", + "value": true + }, + { + "optionDest": "filenames", + "value": "C:/Project Files/brain-dump-ai/backend-homebrew-ai/backends/main.py" + }, + { + "optionDest": "onefile", + "value": false + }, + { + "optionDest": "console", + "value": true + }, + { + "optionDest": "icon_file", + "value": "C:/Project Files/brain-dump-ai/backend-homebrew-ai/public/favicon.ico" + }, + { + "optionDest": "name", + "value": "OpenBrew-Server" + }, + { + "optionDest": "contents_directory", + "value": "_deps" + }, + { + "optionDest": "clean_build", + "value": true + }, + { + "optionDest": "strip", + "value": false + }, + { + "optionDest": "noupx", + "value": false + }, + { + "optionDest": "disable_windowed_traceback", + "value": false + }, + { + "optionDest": "uac_admin", + "value": false + }, + { + "optionDest": "uac_uiaccess", + "value": false + }, + { + "optionDest": "argv_emulation", + "value": false + }, + { + "optionDest": "bootloader_ignore_signals", + "value": false + }, + { + "optionDest": "datas", + "value": "C:/Python311/Lib/site-packages/llama_index/VERSION;./llama_index" + } + ], + "nonPyinstallerOptions": { + "increaseRecursionLimit": true, + "manualArguments": "--mode=dev" + } +} \ No newline at end of file diff --git a/py-to-exe-config.prod.json b/py-to-exe-config.prod.json index 9332920..e5c278d 100644 --- a/py-to-exe-config.prod.json +++ b/py-to-exe-config.prod.json @@ -1,77 +1,77 @@ { - "version": "auto-py-to-exe-configuration_v1", - "pyinstallerOptions": [ - { - "optionDest": "noconfirm", - "value": true - }, - { - "optionDest": "filenames", - "value": "C:/Project Files/brain-dump-ai/backend-homebrew-ai/backends/main.py" - }, - { - "optionDest": "onefile", - "value": false - }, - { - "optionDest": "console", - "value": false - }, - { - "optionDest": "icon_file", - "value": "C:/Project Files/brain-dump-ai/backend-homebrew-ai/public/favicon.ico" - }, - { - "optionDest": "name", - "value": "OpenBrew-Server" - }, - { - "optionDest": "contents_directory", - "value": "_deps" - }, - { - "optionDest": "clean_build", - "value": true - }, - { - "optionDest": "strip", - "value": false - }, - { - "optionDest": "noupx", - "value": false - }, - { - "optionDest": "disable_windowed_traceback", - "value": false - }, - { - "optionDest": "uac_admin", - "value": false - }, - { - "optionDest": "uac_uiaccess", - "value": false - }, - { - "optionDest": "argv_emulation", - "value": false - }, - { - "optionDest": "bootloader_ignore_signals", - "value": false - }, - { - "optionDest": "datas", - "value": "C:/Python311/Lib/site-packages/llama_index/VERSION;./llama_index" - }, - { - "optionDest": "datas", - "value": "C:/Python311/Lib/site-packages/tiktoken_ext;tiktoken_ext/" - } - ], - "nonPyinstallerOptions": { - "increaseRecursionLimit": true, - "manualArguments": "" + "version": "auto-py-to-exe-configuration_v1", + "pyinstallerOptions": [ + { + "optionDest": "noconfirm", + "value": true + }, + { + "optionDest": "filenames", + "value": "C:/Project Files/brain-dump-ai/backend-homebrew-ai/backends/main.py" + }, + { + "optionDest": "onefile", + "value": false + }, + { + "optionDest": "console", + "value": false + }, + { + "optionDest": "icon_file", + "value": "C:/Project Files/brain-dump-ai/backend-homebrew-ai/public/favicon.ico" + }, + { + "optionDest": "name", + "value": "OpenBrew-Server" + }, + { + "optionDest": "contents_directory", + "value": "_deps" + }, + { + "optionDest": "clean_build", + "value": true + }, + { + "optionDest": "strip", + "value": false + }, + { + "optionDest": "noupx", + "value": false + }, + { + "optionDest": "disable_windowed_traceback", + "value": false + }, + { + "optionDest": "uac_admin", + "value": false + }, + { + "optionDest": "uac_uiaccess", + "value": false + }, + { + "optionDest": "argv_emulation", + "value": false + }, + { + "optionDest": "bootloader_ignore_signals", + "value": false + }, + { + "optionDest": "datas", + "value": "C:/Python311/Lib/site-packages/llama_index/VERSION;./llama_index" + }, + { + "optionDest": "datas", + "value": "C:/Python311/Lib/site-packages/tiktoken_ext;tiktoken_ext/" } -} + ], + "nonPyinstallerOptions": { + "increaseRecursionLimit": true, + "manualArguments": "--mode=prod" + } +} \ No newline at end of file From ce5a914db80d9c2dba2a0ef5dfd0afbdac1c5ffc Mon Sep 17 00:00:00 2001 From: DIEHARDERS Date: Thu, 28 Mar 2024 16:54:51 -0700 Subject: [PATCH 2/6] HBAI-209 Edit readme. Edit reqs.txt. Fix main to work with pyinstaller. --- README.md | 8 ++++++-- backends/main.py | 13 +++++++------ package.json | 4 ++-- py-to-exe-config.dev.json | 2 +- py-to-exe-config.prod.json | 2 +- requirements.txt | 8 ++++---- 6 files changed, 21 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 2c66cdd..9421c24 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,9 @@ python src/backends/main.py Or (recommended) ```bash -yarn server +yarn server:dev +# or +yarn server:prod ``` The homebrew api server will be running on [http://localhost:8008](http://localhost:8008) @@ -213,7 +215,9 @@ yarn build Building just the python server for production. This will place the file(s) in `/includes` folder: ```bash -yarn build:api +yarn build:api:dev +# or +yarn build:api:prod ``` ## Release app for distribution diff --git a/backends/main.py b/backends/main.py index 5cf5363..7511d57 100644 --- a/backends/main.py +++ b/backends/main.py @@ -63,8 +63,9 @@ def parse_runtime_args(): buildEnv = parse_runtime_args() -isDev = buildEnv == "dev" -isProd = buildEnv == "prod" +isDebug = hasattr(sys, "gettrace") and sys.gettrace() is not None +isDev = buildEnv == "dev" or isDebug +isProd = buildEnv == "prod" or not isDev if isProd: # Remove prints in prod when deploying in window mode sys.stdout = open(os.devnull, "w") @@ -1260,7 +1261,7 @@ class Window: def __init__(self, master): # @TODO Swap out with a UI or other image self.img = Image.open("public/splash.png") - self.img = self.img.resize((320, 300), Image.FILTERED) + self.img = self.img.resize((640, 480), Image.FILTERED) self.img = ImageTk.PhotoImage(self.img) @@ -1299,6 +1300,6 @@ def run_server(): # GUI window if isProd: run_GUI() - # Handle stopping the server when window is closed - print(f"{common.PRNT_API} Shutting down on user action", flush=True) - os.kill(os.getpid(), signal.SIGINT) + # Handle stopping the server when window is closed + print(f"{common.PRNT_API} Shutting down", flush=True) + os.kill(os.getpid(), signal.SIGINT) diff --git a/package.json b/package.json index 3db598f..d8fbeb7 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ "server:dev": "python ./backends/main.py --mode=dev", "server:prod": "python ./backends/main.py --mode=prod", "build": "yarn run build:api && next build renderer", - "build:api:dev": "yarn run python-deps && pyinstaller --noconfirm --onedir --console --icon C:/Project Files/brain-dump-ai/backend-homebrew-ai/public/favicon.ico --name OpenBrew-Server --contents-directory _deps --clean --debug imports --add-data C:/Python311/Lib/site-packages/llama_index/VERSION;./llama_index C:/Project Files/brain-dump-ai/backend-homebrew-ai/backends/main.py --mode=dev", - "build:api:prod": "yarn run python-deps && pyinstaller --noconfirm --onedir --windowed --icon C:/Project Files/brain-dump-ai/backend-homebrew-ai/public/favicon.ico --name OpenBrew-Server --contents-directory _deps --clean --add-data C:/Python311/Lib/site-packages/llama_index/VERSION;./llama_index --add-data C:/Python311/Lib/site-packages/tiktoken_ext;tiktoken_ext/ C:/Project Files/brain-dump-ai/backend-homebrew-ai/backends/main.py --mode=prod", + "build:api:dev": "yarn run python-deps && pyinstaller --noconfirm --onedir --console --icon C:/Project Files/brain-dump-ai/backend-homebrew-ai/public/favicon.ico --name OpenBrew-Server --contents-directory _deps --clean --debug imports --add-data C:/Python311/Lib/site-packages/llama_index/VERSION;./llama_index C:/Project Files/brain-dump-ai/backend-homebrew-ai/backends/main.py", + "build:api:prod": "yarn run python-deps && pyinstaller --noconfirm --onedir --windowed --icon C:/Project Files/brain-dump-ai/backend-homebrew-ai/public/favicon.ico --name OpenBrew-Server --contents-directory _deps --clean --add-data C:/Python311/Lib/site-packages/llama_index/VERSION;./llama_index --add-data C:/Python311/Lib/site-packages/tiktoken_ext;tiktoken_ext/ C:/Project Files/brain-dump-ai/backend-homebrew-ai/backends/main.py", "python-deps": "pip install -r requirements.txt", "release": "yarn run build && electron-builder", "release:win": "yarn run build && electron-builder --win --x64", diff --git a/py-to-exe-config.dev.json b/py-to-exe-config.dev.json index 5ff6c28..1e79171 100644 --- a/py-to-exe-config.dev.json +++ b/py-to-exe-config.dev.json @@ -68,6 +68,6 @@ ], "nonPyinstallerOptions": { "increaseRecursionLimit": true, - "manualArguments": "--mode=dev" + "manualArguments": "" } } \ No newline at end of file diff --git a/py-to-exe-config.prod.json b/py-to-exe-config.prod.json index e5c278d..ebc81cd 100644 --- a/py-to-exe-config.prod.json +++ b/py-to-exe-config.prod.json @@ -72,6 +72,6 @@ ], "nonPyinstallerOptions": { "increaseRecursionLimit": true, - "manualArguments": "--mode=prod" + "manualArguments": "" } } \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index bafab31..f34b37b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,16 @@ python-dotenv==1.0.0 # For type safety -pydantic_settings==2.0.3 +pydantic-settings==2.0.3 # For downloading models -huggingface_hub==0.21.4 +huggingface-hub==0.21.4 # For servers fastapi==0.101.1 uvicorn==0.23.2 httpx==0.27.0 sse-starlette==1.6.5 -starlette_context==0.3.6 +starlette-context==0.3.6 # For building single exe python -pyinstaller==5.13.0 # this needs security update +pyinstaller==6.5.1 python-multipart==0.0.6 # For text inference llama-cpp-python==0.2.27 From 3edd66db1a62fbe7996b4bc7279b4a9fcc966e06 Mon Sep 17 00:00:00 2001 From: DIEHARDERS Date: Thu, 28 Mar 2024 19:39:48 -0700 Subject: [PATCH 3/6] HBAI-209 Add a GUI interface and display server info --- README.md | 2 +- backends/embedding/chunking.py | 2 +- backends/main.py | 112 ++++++++++++++++++++++++--------- package.json | 6 +- py-to-exe-config.dev.json | 10 ++- py-to-exe-config.prod.json | 4 +- requirements.txt | 1 + 7 files changed, 100 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 9421c24..a23327d 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Forked from vercel [project](https://github.com/vercel/next.js/tree/canary/examp - ✅ Provide easy to use desktop installers - ✅ Embeddings: Create vector embeddings from a text or document files - ✅ Search: Using a vector database and Llama Index to make semantic or similarity queries -- ✅ Advanced Settings: Fine tune how the model is loaded +- ✅ Build custom bots from a mix of LLM's, software configs and prompt configs - ❌ Threads: Save/Retrieve chat message history ## Upcoming Features diff --git a/backends/embedding/chunking.py b/backends/embedding/chunking.py index 6e6a414..ef05ec8 100644 --- a/backends/embedding/chunking.py +++ b/backends/embedding/chunking.py @@ -3,7 +3,7 @@ import os import base64 from typing import List -from PIL import Image +from PIL import Image # use `pip install pillow` instead of PIL from langchain.text_splitter import ( CharacterTextSplitter, RecursiveCharacterTextSplitter, diff --git a/backends/main.py b/backends/main.py index 7511d57..bf4c662 100644 --- a/backends/main.py +++ b/backends/main.py @@ -81,21 +81,6 @@ def parse_runtime_args(): @asynccontextmanager async def lifespan(application: FastAPI): print(f"{common.PRNT_API} Lifespan startup", flush=True) - # Display where the admin can use the web UI - openbrew_studio_url = "https://studio.openbrewai.com" - print( - f"{common.PRNT_API} Navigate your browser to OpenBrew Studio\n-> {openbrew_studio_url} for the admin web UI.", - flush=True, - ) - # Display the local IP address of this server - hostname = socket.gethostname() - IPAddr = socket.gethostbyname(hostname) - openbrew_server_ip = f"http://{IPAddr}:{SERVER_PORT}/docs" - openbrew_server_local_ip = f"http://localhost:{SERVER_PORT}/docs" - print( - f"{common.PRNT_API} Refer to API docs for OpenBrew Server \n-> {openbrew_server_local_ip} \nOR\n-> {openbrew_server_ip}", - flush=True, - ) # https://www.python-httpx.org/quickstart/ app.requests_client = httpx.Client() # Store some state here if you want... @@ -1257,23 +1242,86 @@ def get_bot_settings() -> classes.BotSettingsResponse: # Methods... -class Window: - def __init__(self, master): - # @TODO Swap out with a UI or other image - self.img = Image.open("public/splash.png") - self.img = self.img.resize((640, 480), Image.FILTERED) - - self.img = ImageTk.PhotoImage(self.img) - - label = tk.Label(master, image=self.img) - label.pack(expand=True, fill=tk.BOTH) +def display_server_info(): + # Display where the admin can use the web UI + openbrew_studio_url = "https://studio.openbrewai.com" + print( + f"{common.PRNT_API} Navigate your browser to OpenBrew Studio\n-> {openbrew_studio_url} for the admin web UI.", + flush=True, + ) + # Display the local IP address of this server + hostname = socket.gethostname() + IPAddr = socket.gethostbyname(hostname) + remote_ip = f"http://{IPAddr}:{SERVER_PORT}/docs" + local_ip = f"http://localhost:{SERVER_PORT}/docs" + print( + f"{common.PRNT_API} Refer to API docs for OpenBrew Server \n-> {local_ip} \nOR\n-> {remote_ip}", + flush=True, + ) + return { + "local_ip": local_ip, + "remote_ip": remote_ip, + "web_ui_address": openbrew_studio_url, + } # Function to create and run the Tkinter window -def run_GUI(): +def run_GUI(local_ip: str, remote_ip: str, webui_address: str): + color_bg = "#333333" + color_label = "#ffe135" root = tk.Tk() root.title("OpenBrew Server") - window = Window(root) + root.geometry("640x480") + # since /public folder is bundled inside _deps, we need to read from root `sys._MEIPASS` + root.iconbitmap(default=os.path.join(sys._MEIPASS, "public/favicon.ico")) + root.configure(bg=color_bg) + frame = tk.Frame(bg=color_bg) + # Labels + title_label = tk.Label( + frame, + text="Server Info", + bg=color_bg, + fg=color_label, + font=("Arial", 30), + ) + local_label = tk.Label( + frame, + text="API Docs (Local)", + bg=color_bg, + fg=color_label, + font=("Arial", 16), + ) + remote_label = tk.Label( + frame, + text="API Docs (Remote)", + bg=color_bg, + fg=color_label, + font=("Arial", 16), + ) + webui_label = tk.Label( + frame, + text="WebUI Address", + bg=color_bg, + fg=color_label, + font=("Arial", 16), + ) + # Inputs + local_entry = tk.Entry(frame, font=("Arial", 24)) + local_entry.insert(0, local_ip) + remote_entry = tk.Entry(frame, font=("Arial", 24)) + remote_entry.insert(0, remote_ip) + webui_entry = tk.Entry(frame, font=("Arial", 24)) + webui_entry.insert(0, webui_address) + # Placement + title_label.grid(row=0, column=0, columnspan=2, sticky="news", pady=40) + local_label.grid(row=1, column=0, padx=20) + local_entry.grid(row=1, column=1, pady=20) + remote_label.grid(row=2, column=0, padx=20) + remote_entry.grid(row=2, column=1, pady=20) + webui_label.grid(row=3, column=0, padx=20) + webui_entry.grid(row=3, column=1, pady=20) + frame.pack() + # Render root.mainloop() @@ -1297,9 +1345,15 @@ def run_server(): # Start the API server in a separate thread fastapi_thread = threading.Thread(target=run_server) fastapi_thread.start() - # GUI window + # Find IP info + server_info = display_server_info() + # Render GUI window if isProd: - run_GUI() + run_GUI( + local_ip=server_info["local_ip"], + remote_ip=server_info["remote_ip"], + webui_address=server_info["web_ui_address"], + ) # Handle stopping the server when window is closed print(f"{common.PRNT_API} Shutting down", flush=True) os.kill(os.getpid(), signal.SIGINT) diff --git a/package.json b/package.json index d8fbeb7..9a51d73 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openbrew-server", - "version": "0.3.1", + "version": "0.3.2", "author": "Spread Shot Studios", "license": "MIT", "productName": "OpenBrew Server", @@ -13,8 +13,8 @@ "server:dev": "python ./backends/main.py --mode=dev", "server:prod": "python ./backends/main.py --mode=prod", "build": "yarn run build:api && next build renderer", - "build:api:dev": "yarn run python-deps && pyinstaller --noconfirm --onedir --console --icon C:/Project Files/brain-dump-ai/backend-homebrew-ai/public/favicon.ico --name OpenBrew-Server --contents-directory _deps --clean --debug imports --add-data C:/Python311/Lib/site-packages/llama_index/VERSION;./llama_index C:/Project Files/brain-dump-ai/backend-homebrew-ai/backends/main.py", - "build:api:prod": "yarn run python-deps && pyinstaller --noconfirm --onedir --windowed --icon C:/Project Files/brain-dump-ai/backend-homebrew-ai/public/favicon.ico --name OpenBrew-Server --contents-directory _deps --clean --add-data C:/Python311/Lib/site-packages/llama_index/VERSION;./llama_index --add-data C:/Python311/Lib/site-packages/tiktoken_ext;tiktoken_ext/ C:/Project Files/brain-dump-ai/backend-homebrew-ai/backends/main.py", + "build:api:dev": "yarn run python-deps && pyinstaller --noconfirm --onedir --console --icon C:/Project Files/brain-dump-ai/backend-homebrew-ai/public/favicon.ico --name OpenBrew-Server --contents-directory _deps --clean --debug bootloader --add-data C:/Python311/Lib/site-packages/llama_index/VERSION;llama_index --add-data C:/Project Files/brain-dump-ai/backend-homebrew-ai/public;public/ C:/Project Files/brain-dump-ai/backend-homebrew-ai/backends/main.py", + "build:api:prod": "yarn run python-deps && pyinstaller --noconfirm --onedir --windowed --icon C:/Project Files/brain-dump-ai/backend-homebrew-ai/public/favicon.ico --name OpenBrew-Server --contents-directory _deps --clean --add-data C:/Python311/Lib/site-packages/llama_index/VERSION;llama_index --add-data C:/Project Files/brain-dump-ai/backend-homebrew-ai/public;public/ C:/Project Files/brain-dump-ai/backend-homebrew-ai/backends/main.py", "python-deps": "pip install -r requirements.txt", "release": "yarn run build && electron-builder", "release:win": "yarn run build && electron-builder --win --x64", diff --git a/py-to-exe-config.dev.json b/py-to-exe-config.dev.json index 1e79171..7cbfcf2 100644 --- a/py-to-exe-config.dev.json +++ b/py-to-exe-config.dev.json @@ -33,6 +33,10 @@ "optionDest": "clean_build", "value": true }, + { + "optionDest": "debug", + "value": "bootloader" + }, { "optionDest": "strip", "value": false @@ -63,7 +67,11 @@ }, { "optionDest": "datas", - "value": "C:/Python311/Lib/site-packages/llama_index/VERSION;./llama_index" + "value": "C:/Python311/Lib/site-packages/llama_index/VERSION;llama_index" + }, + { + "optionDest": "datas", + "value": "C:/Project Files/brain-dump-ai/backend-homebrew-ai/public;public/" } ], "nonPyinstallerOptions": { diff --git a/py-to-exe-config.prod.json b/py-to-exe-config.prod.json index ebc81cd..e37fb86 100644 --- a/py-to-exe-config.prod.json +++ b/py-to-exe-config.prod.json @@ -63,11 +63,11 @@ }, { "optionDest": "datas", - "value": "C:/Python311/Lib/site-packages/llama_index/VERSION;./llama_index" + "value": "C:/Python311/Lib/site-packages/llama_index/VERSION;llama_index" }, { "optionDest": "datas", - "value": "C:/Python311/Lib/site-packages/tiktoken_ext;tiktoken_ext/" + "value": "C:/Project Files/brain-dump-ai/backend-homebrew-ai/public;public/" } ], "nonPyinstallerOptions": { diff --git a/requirements.txt b/requirements.txt index f34b37b..ebe0b61 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +# Misc python-dotenv==1.0.0 # For type safety pydantic-settings==2.0.3 From 14bbbda9b4cdf3398aca97e4fefadbe4c6b4c611 Mon Sep 17 00:00:00 2001 From: DIEHARDERS Date: Fri, 29 Mar 2024 11:04:59 -0700 Subject: [PATCH 4/6] HBAI-209 Fix sys path crash. Add link to tool in GUI. --- backends/main.py | 77 +++++++++++++++++++++++++++------------ backends/server/common.py | 14 +++++++ 2 files changed, 68 insertions(+), 23 deletions(-) diff --git a/backends/main.py b/backends/main.py index bf4c662..03dd7cb 100644 --- a/backends/main.py +++ b/backends/main.py @@ -5,6 +5,7 @@ import glob import json import uvicorn +import webbrowser import httpx import shutil import socket @@ -21,7 +22,6 @@ from fastapi.middleware.cors import CORSMiddleware from sse_starlette.sse import EventSourceResponse import tkinter as tk -from PIL import Image, ImageTk from contextlib import asynccontextmanager from inference import text_llama_index from embedding import embedding @@ -1252,8 +1252,8 @@ def display_server_info(): # Display the local IP address of this server hostname = socket.gethostname() IPAddr = socket.gethostbyname(hostname) - remote_ip = f"http://{IPAddr}:{SERVER_PORT}/docs" - local_ip = f"http://localhost:{SERVER_PORT}/docs" + remote_ip = f"http://{IPAddr}:{SERVER_PORT}" + local_ip = f"http://localhost:{SERVER_PORT}" print( f"{common.PRNT_API} Refer to API docs for OpenBrew Server \n-> {local_ip} \nOR\n-> {remote_ip}", flush=True, @@ -1271,9 +1271,9 @@ def run_GUI(local_ip: str, remote_ip: str, webui_address: str): color_label = "#ffe135" root = tk.Tk() root.title("OpenBrew Server") - root.geometry("640x480") + root.geometry("1200x600") # since /public folder is bundled inside _deps, we need to read from root `sys._MEIPASS` - root.iconbitmap(default=os.path.join(sys._MEIPASS, "public/favicon.ico")) + root.iconbitmap(default=os.path.join(common.dep_path("public/favicon.ico"))) root.configure(bg=color_bg) frame = tk.Frame(bg=color_bg) # Labels @@ -1284,42 +1284,73 @@ def run_GUI(local_ip: str, remote_ip: str, webui_address: str): fg=color_label, font=("Arial", 30), ) - local_label = tk.Label( + descr_label = tk.Label( frame, - text="API Docs (Local)", + text="Click the link below or navigate your browser to use the WebUI interface.", + bg=color_bg, + fg="white", + font=("Arial", 14), + ) + docs_label = tk.Label( + frame, + text="API Docs:", + bg=color_bg, + fg=color_label, + font=("Arial", 24), + width=24, + ) + server_local_label = tk.Label( + frame, + text="Server (Local Address):", bg=color_bg, fg=color_label, - font=("Arial", 16), + font=("Arial", 24), + width=24, ) remote_label = tk.Label( frame, - text="API Docs (Remote)", + text="Server (Remote Address):", bg=color_bg, fg=color_label, - font=("Arial", 16), + font=("Arial", 24), + width=24, ) webui_label = tk.Label( frame, - text="WebUI Address", + text="WebUI Address:", bg=color_bg, fg=color_label, - font=("Arial", 16), + font=("Arial", 24), + width=24, + ) + webui_link = tk.Label( + frame, + text=webui_address, + bg=color_bg, + fg="white", + font=("Arial", 24), + cursor="hand2", + width=24, ) + webui_link.bind("", lambda e: webbrowser.open_new_tab(webui_address)) # Inputs - local_entry = tk.Entry(frame, font=("Arial", 24)) - local_entry.insert(0, local_ip) - remote_entry = tk.Entry(frame, font=("Arial", 24)) + docs_entry = tk.Entry(frame, font=("Arial", 24), w="24") + docs_entry.insert(0, f"{local_ip}/docs") + server_local_entry = tk.Entry(frame, font=("Arial", 24), w="24") + server_local_entry.insert(0, f"{local_ip}") + remote_entry = tk.Entry(frame, font=("Arial", 24), w="24") remote_entry.insert(0, remote_ip) - webui_entry = tk.Entry(frame, font=("Arial", 24)) - webui_entry.insert(0, webui_address) # Placement title_label.grid(row=0, column=0, columnspan=2, sticky="news", pady=40) - local_label.grid(row=1, column=0, padx=20) - local_entry.grid(row=1, column=1, pady=20) - remote_label.grid(row=2, column=0, padx=20) - remote_entry.grid(row=2, column=1, pady=20) - webui_label.grid(row=3, column=0, padx=20) - webui_entry.grid(row=3, column=1, pady=20) + descr_label.grid(row=1, column=0, columnspan=2, sticky="news", pady=40) + webui_label.grid(row=2, column=0, padx=20) + webui_link.grid(row=2, column=1, pady=20) + server_local_label.grid(row=3, column=0, padx=20) + server_local_entry.grid(row=3, column=1, padx=20) + remote_label.grid(row=4, column=0, padx=20) + remote_entry.grid(row=4, column=1, pady=20) + docs_label.grid(row=5, column=0, padx=20) + docs_entry.grid(row=5, column=1, pady=20) frame.pack() # Render root.mainloop() diff --git a/backends/server/common.py b/backends/server/common.py index 957476d..1adbc60 100644 --- a/backends/server/common.py +++ b/backends/server/common.py @@ -1,4 +1,5 @@ import re +import sys import os import json import glob @@ -502,3 +503,16 @@ def read_constants(app): with open(path, "r") as json_file: data = json.load(json_file) app.PORT_HOMEBREW_API = data["PORT_HOMEBREW_API"] + + +# Pass a relative path to resource and return the correct absolute path. Works for dev and for PyInstaller +# If you use pyinstaller, it bundles deps into a folder alongside the binary (not --onefile mode). +# This path is set to sys._MEIPASS and any python modules or added files are put in here (runtime writes, db still go where they should). +def dep_path(relative_path): + try: + # PyInstaller creates a temp folder and stores path in _MEIPASS + base_path = sys._MEIPASS + except Exception: + base_path = os.path.abspath(".") + + return os.path.join(base_path, relative_path) From bf812699a93ba59bc6186db2b2d1bfbcd0ac6a7f Mon Sep 17 00:00:00 2001 From: DIEHARDERS Date: Fri, 29 Mar 2024 14:07:55 -0700 Subject: [PATCH 5/6] HBAI-209 Fixed terminating server --- backends/main.py | 52 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/backends/main.py b/backends/main.py index 03dd7cb..3e930fe 100644 --- a/backends/main.py +++ b/backends/main.py @@ -36,6 +36,7 @@ HfApi, ) +server_thread = None VECTOR_DB_FOLDER = "chromadb" MEMORY_FOLDER = "memories" PARSED_FOLDER = "parsed" @@ -93,7 +94,7 @@ async def lifespan(application: FastAPI): app.state.loaded_text_model_data = {} yield - + # Do shutdown cleanup here... print(f"{common.PRNT_API} Lifespan shutdown") @@ -1242,11 +1243,19 @@ def get_bot_settings() -> classes.BotSettingsResponse: # Methods... +def shutdown_server(*args): + print(f"{common.PRNT_API} Shutting down server...", flush=True) + # os.kill(os.getpid(), signal.SIGINT) + # server_thread.join() + print(f"{common.PRNT_API} Server shutdown complete.", flush=True) + sys.exit(0) + + def display_server_info(): # Display where the admin can use the web UI openbrew_studio_url = "https://studio.openbrewai.com" print( - f"{common.PRNT_API} Navigate your browser to OpenBrew Studio\n-> {openbrew_studio_url} for the admin web UI.", + f"{common.PRNT_API} Navigate your browser to OpenBrew Studio for the admin web UI:\n-> {openbrew_studio_url}", flush=True, ) # Display the local IP address of this server @@ -1255,7 +1264,7 @@ def display_server_info(): remote_ip = f"http://{IPAddr}:{SERVER_PORT}" local_ip = f"http://localhost:{SERVER_PORT}" print( - f"{common.PRNT_API} Refer to API docs for OpenBrew Server \n-> {local_ip} \nOR\n-> {remote_ip}", + f"{common.PRNT_API} Refer to API docs for OpenBrew Server:\n-> {local_ip} \nOR\n-> {remote_ip}", flush=True, ) return { @@ -1267,6 +1276,8 @@ def display_server_info(): # Function to create and run the Tkinter window def run_GUI(local_ip: str, remote_ip: str, webui_address: str): + if not isProd: + return color_bg = "#333333" color_label = "#ffe135" root = tk.Tk() @@ -1354,9 +1365,12 @@ def run_GUI(local_ip: str, remote_ip: str, webui_address: str): frame.pack() # Render root.mainloop() + # Handle stopping the server when window is closed + print(f"{common.PRNT_API} Shutting down GUI", flush=True) + shutdown_server() -def run_server(): +def start_server(): try: print(f"{common.PRNT_API} Starting API server...") # Start the ASGI server @@ -1372,19 +1386,29 @@ def run_server(): return False -if __name__ == "__main__": - # Start the API server in a separate thread - fastapi_thread = threading.Thread(target=run_server) +def run_server(): + # Start the API server in a separate thread from GUI + fastapi_thread = threading.Thread(target=start_server) + fastapi_thread.daemon = True # let the parent kill the child thread at exit fastapi_thread.start() - # Find IP info - server_info = display_server_info() - # Render GUI window - if isProd: + return fastapi_thread + + +if __name__ == "__main__": + try: + # Start API server + server_thread = run_server() + # Find IP info + server_info = display_server_info() + # Render GUI window run_GUI( local_ip=server_info["local_ip"], remote_ip=server_info["remote_ip"], webui_address=server_info["web_ui_address"], ) - # Handle stopping the server when window is closed - print(f"{common.PRNT_API} Shutting down", flush=True) - os.kill(os.getpid(), signal.SIGINT) + # Prevent main process from closing prematurely + while True: + pass + except KeyboardInterrupt: + print(f"{common.PRNT_API} User pressed Ctrl+C exiting...") + shutdown_server() From effed304e8924dd12921ef495821859d6ca6b0f5 Mon Sep 17 00:00:00 2001 From: DIEHARDERS Date: Fri, 29 Mar 2024 14:25:51 -0700 Subject: [PATCH 6/6] HBAI-209 Open web address on startup --- backends/main.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/backends/main.py b/backends/main.py index 3e930fe..b5fa109 100644 --- a/backends/main.py +++ b/backends/main.py @@ -1,6 +1,5 @@ import os import sys -import signal import threading import glob import json @@ -48,6 +47,8 @@ PLAYGROUND_SETTINGS_FILE_NAME = "playground.json" BOT_SETTINGS_FILE_NAME = "bots.json" SERVER_PORT = 8008 +# Display where the admin can use the web UI +openbrew_studio_url = "https://studio.openbrewai.com" # Parse runtime arguments passed to script @@ -1252,8 +1253,6 @@ def shutdown_server(*args): def display_server_info(): - # Display where the admin can use the web UI - openbrew_studio_url = "https://studio.openbrewai.com" print( f"{common.PRNT_API} Navigate your browser to OpenBrew Studio for the admin web UI:\n-> {openbrew_studio_url}", flush=True, @@ -1270,12 +1269,11 @@ def display_server_info(): return { "local_ip": local_ip, "remote_ip": remote_ip, - "web_ui_address": openbrew_studio_url, } # Function to create and run the Tkinter window -def run_GUI(local_ip: str, remote_ip: str, webui_address: str): +def run_GUI(local_ip: str, remote_ip: str): if not isProd: return color_bg = "#333333" @@ -1336,14 +1334,16 @@ def run_GUI(local_ip: str, remote_ip: str, webui_address: str): ) webui_link = tk.Label( frame, - text=webui_address, + text=openbrew_studio_url, bg=color_bg, fg="white", font=("Arial", 24), cursor="hand2", width=24, ) - webui_link.bind("", lambda e: webbrowser.open_new_tab(webui_address)) + webui_link.bind( + "", lambda e: webbrowser.open_new_tab(openbrew_studio_url) + ) # Inputs docs_entry = tk.Entry(frame, font=("Arial", 24), w="24") docs_entry.insert(0, f"{local_ip}/docs") @@ -1380,10 +1380,8 @@ def start_server(): port=SERVER_PORT, log_level="info", ) - return True except: print(f"{common.PRNT_API} Failed to start API server") - return False def run_server(): @@ -1404,8 +1402,12 @@ def run_server(): run_GUI( local_ip=server_info["local_ip"], remote_ip=server_info["remote_ip"], - webui_address=server_info["web_ui_address"], ) + # Open browser to WebUI + print( + f"{common.PRNT_API} API server started. Opening WebUI at {openbrew_studio_url}" + ) + webbrowser.open(openbrew_studio_url, new=2) # Prevent main process from closing prematurely while True: pass