From e49ade62d0ed7c5edc5d15cb5c89a620fd4bc306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lyx=20Rothbo=CC=88ck?= Date: Sat, 24 Feb 2024 04:13:13 +0100 Subject: [PATCH 1/5] Usability and UI Improvements Moved install check into its own function; Moved model install into its own function; Added icons depending on status of each model; Added UI auto refresh for icons; Added alerts to ask if the user wants to install a model, and to tell them it is installing; Use .lock files to indicate installation process; Setting title of widget to currently used model; Added timeout for set_target request to 1 Second to avoid the app freezing; Moved default model config to new settings object; Automated killing of process on 5001 if the last script crased (requires psutil); Added automatic switching between online and offline using "default_online" and "default_offline"; --- app.py | 111 ++++++++++++++++++++++++++++----- config.py | 12 +++- proxy.py | 14 ++++- requirements.txt | 1 + icon.png => resources/icon.png | Bin resources/installed.png | Bin 0 -> 3730 bytes resources/installing.png | Bin 0 -> 3908 bytes resources/uninstalled.png | Bin 0 -> 3778 bytes 8 files changed, 118 insertions(+), 20 deletions(-) rename icon.png => resources/icon.png (100%) create mode 100644 resources/installed.png create mode 100644 resources/installing.png create mode 100644 resources/uninstalled.png diff --git a/app.py b/app.py index 9d06a3a..085a91d 100644 --- a/app.py +++ b/app.py @@ -8,23 +8,58 @@ import config +ICON = "resources/icon.png" +ICON_INSTALLED = "resources/installed.png" +ICON_INSTALLING = "resources/installing.png" +ICON_UNINSTALLED = "resources/uninstalled.png" + + def setup(): if not os.path.exists(config.model_folder): if input(f"Model folder {config.model_folder} does not exist. Create it? (y/n) ").lower() == 'y': os.mkdir(config.model_folder) - current = os.listdir(config.model_folder) for model in config.models: if model == 'default': continue - if config.models[model]['type'] == 'local': - if config.models[model]['filename'] not in current: - if input(f'Model {model} not found in {config.model_folder}. Would you like to download it? (y/n) ').lower() == 'y': - url = config.models[model]['url'] - print(f"Downloading {model} from {url}...") - subprocess.run(['curl', '-L', url, '-o', os.path.join( - config.model_folder, config.models[model]['filename'])]) - else: - print(f"Model {model} found in {config.model_folder}.") + if is_installed(model): + print(f"Model {model} found in {config.model_folder}.") + else: + if input(f'Model {model} not found in {config.model_folder}. Would you like to download it? (y/n) ').lower() == 'y': + install_model(model, True) + + +def is_installed(model): + if config.models[model]['type'] == 'local': + return os.path.exists(config.model_folder + "/" + config.models[model]['filename']) + else: + return True + + +def install_model(model, verbose = False, app = None): + url = config.models[model]['url'] + if verbose: + print(f"Downloading {model} from {url}...") + + if app is not None: + lock_model(model) + subprocess.run([ + 'osascript', + '-e', + # install the model in a new terminal window, then delete the lock file + f'tell application "Terminal" to do script "curl -L {url} -o {os.path.join(config.model_folder, config.models[model]["filename"])} && rm {config.model_folder}/{model}.lock"']) + return + else: + subprocess.run(['curl', '-L', url, '-o', os.path.join( + config.model_folder, config.models[model]['filename'])]) + +def lock_model(model): + with open(f"{config.model_folder}/{model}.lock", 'w') as f: + f.write("") + +def is_installing(model): + if config.models[model]['type'] == 'remote': + return False + return os.path.exists(f"{config.model_folder}/{model}.lock") class ModelPickerApp(rumps.App): @@ -34,25 +69,67 @@ def __init__(self): # Dynamically create menu items from the MENUBAR_OPTIONS self.menu_items = {} for option in config.models: - if option == 'default': - continue self.menu_items[option] = rumps.MenuItem( - title=option, callback=self.pick_model) + title=option, callback=self.pick_model, icon=None) self.menu = list(self.menu_items.values()) - self.menu_items[config.models['default']].state = True - self.icon = "icon.png" + self.menu_items[config.settings['default_online']].state = True + self.title = config.settings['default_online'] + self.icon = ICON + rumps.Timer(self.update_menu, 5).start() + + def update_menu(self, sender): + for option in self.menu_items: + if is_installing(option): + self.menu_items[option].icon = ICON_INSTALLING + elif is_installed(option): + self.menu_items[option].icon = ICON_INSTALLED + else: + self.menu_items[option].icon = ICON_UNINSTALLED + + currently_online = config.models[self.title]['type'] == 'remote' + + if currently_online and config.settings['switch'] == 'automatic' or config.settings['switch'] == 'trigger_offline': + try: + response = requests.get("http://google.com", timeout=1.0) + if response.status_code != 200: + self.pick_model(self.menu_items[config.settings['default_offline']]) + except requests.RequestException as e: + self.pick_model(self.menu_items[config.settings['default_offline']]) + + if not currently_online and config.settings['switch'] == 'automatic': + try: + response = requests.get("http://google.com", timeout=1.0) + if response.status_code == 200: + self.pick_model(self.menu_items[config.settings['default_online']]) + except requests.RequestException as e: + pass def pick_model(self, sender): + # check if the model is installed + if is_installing(sender.title): + rumps.alert("Model Installing", f"{sender.title} is currently installing.") + return + elif not is_installed(sender.title): + if (rumps.alert("Install Model", f"Install {sender.title}?", cancel = True) == 1): + install_model(sender.title, app = self) + return + else: + return + + if (sender.state): + return + # Toggle the checked status of the clicked menu item - sender.state = not sender.state + sender.state = True + self.title = sender.title # Send the choice to the local proxy app if sender.state: choice = sender.title try: response = requests.post( - "http://localhost:5001/set_target", json={"target": choice}) + "http://localhost:5001/set_target", json={"target": choice}, timeout=1.0) if response.status_code == 200: print(f"Successfully sent selection: {choice}.") else: diff --git a/config.py b/config.py index 9b11232..337d345 100644 --- a/config.py +++ b/config.py @@ -19,8 +19,16 @@ 'url': 'https://huggingface.co/TheBloke/CodeLlama-34B-Instruct-GGUF/resolve/main/codellama-34b-instruct.Q4_K_M.gguf', 'type': 'local', 'filename': 'codellama-34b-instruct.Q4_K_M.gguf', - }, - 'default': 'GitHub', + } +} + +settings = { + 'default_online': 'GitHub', + 'default_offline': 'CodeLlama-7b', + # automatic -> switch to default_online if online, switch to default_offline if offline + # trigger_offline -> switch to default_offline if offline, don't switch if online + # manual -> don't switch + 'switch': 'automatic', } model_folder = os.path.expanduser('~/models') diff --git a/proxy.py b/proxy.py index 09d488c..eb83044 100644 --- a/proxy.py +++ b/proxy.py @@ -8,7 +8,7 @@ import config app = applications.Starlette() -state = config.models[config.models['default']] +state = config.models[config.settings['default_online']] local_server_process = None logging.basicConfig(level=logging.DEBUG) @@ -86,4 +86,16 @@ async def server_error(request, exc): if __name__ == '__main__': import uvicorn + import psutil + + # kill any existing local server on 5001 + for proc in psutil.process_iter(): + try: + for conns in proc.connections(kind='inet'): + if conns.laddr.port == 5001: + print(f"Killing process {proc.name()} on port 5001") + proc.kill() + except: + pass + uvicorn.run(app, host="0.0.0.0", port=5001) diff --git a/requirements.txt b/requirements.txt index 980b630..ea55803 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,7 @@ httpx==0.25.0 idna==3.4 llama_cpp_python==0.2.11 numpy==1.26.0 +psutil==5.9.8 pydantic==2.4.2 pydantic-settings==2.0.3 pydantic_core==2.10.1 diff --git a/icon.png b/resources/icon.png similarity index 100% rename from icon.png rename to resources/icon.png diff --git a/resources/installed.png b/resources/installed.png new file mode 100644 index 0000000000000000000000000000000000000000..7deacc8f08b200a2308e8717ffc985b54e3b9765 GIT binary patch literal 3730 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%0AWURxz)9>%=z5W^Lqyx zE#1h`LwR#nvMTM5KS^v}tzJQQpp-GW)T9d{d6-LE$<5jUQPT_grN)6xW=j$+>#O0o z8x7N?es5fAL>q=&3_bsS4^_72B;FN|523GbQvQ;PPJ69D=Oxk%u&Ue+XhV_GqySf?~XQL8k^7YRUNi3xb?Jv>=hz(E2geE zPD2Ie5@BIQ<#m`&)2Jd@{FR6AZk_7#TYVt3XGZ3sxvi3iz>q~6m(bq2|4terb6_f_ z`plbd*ho{(Df;EE7yDxo?HFlf9K7BAhr7+;j?esuw>V_MuyaG*CsVJ5x~v|&G343$ z!gO`P(Yj4Nq`|_cMw4%4o?#Dr)UqB!hu9=R09^zClg)J(4A@ihEdYFD5$Nj`E-rc4 zvt*0SGeql?7xt(H)crhb>bsedmnV$uY0B=8G4U=qzpHEU`Gb^)J*GkEWiz|{k{;A9 zPd#+6&AZx9V|PItT65p6@NnW(!|J-a)PM-19U&%7*(5YmE=gv~hkkq7u=?>aRl4V^ z^J&9-OUX^Jz#Vh`>CZv;>Or6@OHl%QKE-o=rhf)JvvjW~;O?I83G~8vz+$zTrwr|pkxqCyK_0387Sn1cs3s&Wym!CRhgiHPr_YXd07uqAp;6|c-A z?hIoDR^n2kgYtB+gLG!O{Wbs#8&xV@nNvL|@~9kRTpoJ(=jnG^RXM$~Gq`TFs@#5~1%esr zVXHb~Kcu$-{$@TQX#0%Jfwr1=6nHVyqLzBE?lHcP1{TIVRZlv)P}Pc3mD@Kh>B^jy zmw$2UwM_c(gT_H*fd-}8Z*KzQ11_Cmgm@zlKY8PH1oI+I`(t^rc2Qm&i3jKNyjS!!Z6^!8Bw%Qw422InMG8O8`gSB~W z^l3jhSY-HAx#9>WM^ycR=(X0y06E9F)4yG#44QoR59!;g<5$-Gs#anK-L2T#PSR#O z-lfy(OP#Y{pS6@syZ5$lqsu83DY$ZZ-|T&>t2!(tu5*vK+&Lya5zw?o=vU>?c=j$C zd|_l-5RZ6?kv!*s+dTz-RYUf>#3jkw&#rQ#XLMgoL9g*O0IknS2>rrQ8e_ zy9^89$6I8{vPaD;CQ6AV*~w434SZ@@efArn(1hvTT$J^e7Hg%ET&ZT zTkF@K{@mt#X~n{B3yAmAQ9t+v1L>+})M9frDq?iZ7pL3MqIS>1B`H5U&ZlYDJR*@Ilu{N4QCe-QN!azo&s2RW+K^wdQ9;)>C3-+4Tl#(-3jFNAxCl&aP`mnH2Qm zU1uQ}x#vfFD{iBOB%7`?Yab*+%C0i-lo~sKe>ISCbT(RukUYl=<`K?LtMiqoPemw9 z(8BcSEGu$r!*|kEw`#wpz1tCp#j;UC9XEE**Q3HCDkGx!wV19N-?)P&G9o zwY1YYb#RL0xoXg~K#g)Ako4sZnv!rPZwQ2zGL1q%xKPnO>5aJKt8c15m#Plv#A`$ujzYlN{I##@#>WvUWXMxc6dP3BC{VwTPWDYkgd0l>C8Jks{6T}%v*|5 zOGnJ=Czsht()4{>Y1%4>bE6rpf#q^mT6S+`*~02}{i1Y-I2B#%IqzJ3rhZxdnf{Dy z>$h2~rE5F%Cu?s!%-Yx#87CV(Y(E6oomg=4EFhWp8tCpGd1Xq|<4xIsF^UeK`%fR6 dI{)0)^2 literal 0 HcmV?d00001 diff --git a/resources/uninstalled.png b/resources/uninstalled.png new file mode 100644 index 0000000000000000000000000000000000000000..87c8ad5f819a48cc8450ca9719cd358d2d3b2c5c GIT binary patch literal 3778 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 E0-MDg9{>OV literal 0 HcmV?d00001 From 4b975f592633fdbc168a60a2bef4411bc17a11e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lyx=20Rothbo=CC=88ck?= Date: Sun, 25 Feb 2024 14:06:14 +0100 Subject: [PATCH 2/5] Display "Not installed" on remotes when offline Removed unnecessary check; Added install check (ping) to remote models; --- app.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/app.py b/app.py index 085a91d..a58232c 100644 --- a/app.py +++ b/app.py @@ -19,8 +19,6 @@ def setup(): if input(f"Model folder {config.model_folder} does not exist. Create it? (y/n) ").lower() == 'y': os.mkdir(config.model_folder) for model in config.models: - if model == 'default': - continue if is_installed(model): print(f"Model {model} found in {config.model_folder}.") else: @@ -32,7 +30,11 @@ def is_installed(model): if config.models[model]['type'] == 'local': return os.path.exists(config.model_folder + "/" + config.models[model]['filename']) else: - return True + try: + response = requests.get("http://google.com", timeout=1.0) + return response.status_code == 200 + except requests.RequestException as e: + return False def install_model(model, verbose = False, app = None): @@ -57,9 +59,10 @@ def lock_model(model): f.write("") def is_installing(model): - if config.models[model]['type'] == 'remote': + if config.models[model]['type'] == 'local': + return os.path.exists(f"{config.model_folder}/{model}.lock") + else: return False - return os.path.exists(f"{config.model_folder}/{model}.lock") class ModelPickerApp(rumps.App): @@ -111,6 +114,8 @@ def pick_model(self, sender): rumps.alert("Model Installing", f"{sender.title} is currently installing.") return elif not is_installed(sender.title): + if config.models[sender.title]['type'] == 'remote': + return if (rumps.alert("Install Model", f"Install {sender.title}?", cancel = True) == 1): install_model(sender.title, app = self) return From 148bae8cfa956662b9be5daba8523be653c499c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lyx=20Rothbo=CC=88ck?= Date: Sun, 25 Feb 2024 16:22:35 +0100 Subject: [PATCH 3/5] Added persistent settings Settings are now changeable in UI and save to json; --- app.py | 76 +++++++++++++++++++++++++++++++++++++++------------ config.py | 9 ------ proxy.py | 10 +++++-- settings.json | 5 ++++ 4 files changed, 72 insertions(+), 28 deletions(-) create mode 100644 settings.json diff --git a/app.py b/app.py index a58232c..23e1940 100644 --- a/app.py +++ b/app.py @@ -1,3 +1,4 @@ +import json import rumps import requests import threading @@ -63,7 +64,21 @@ def is_installing(model): return os.path.exists(f"{config.model_folder}/{model}.lock") else: return False + +def set_settings(setting, value): + with open('settings.json', 'r') as f: + settings = json.load(f) + settings[setting] = value + + with open('settings.json', 'w') as f: + json.dump(settings, f, indent=4) + +def get_settings(setting): + with open('settings.json', 'r') as f: + settings = json.load(f) + + return settings[setting] class ModelPickerApp(rumps.App): def __init__(self): @@ -74,15 +89,43 @@ def __init__(self): for option in config.models: self.menu_items[option] = rumps.MenuItem( title=option, callback=self.pick_model, icon=None) - + + self.menu_items['Settings'] = rumps.MenuItem( + title='Settings', icon=None) + + self.add_settings_menu('Switch', ['Automatic', 'Trigger Offline', 'Manual']) + self.add_settings_menu('Default Online', filter(lambda x: config.models[x]['type'] == 'remote', config.models)) + self.add_settings_menu('Default Offline', filter(lambda x: config.models[x]['type'] == 'local', config.models)) + self.menu = list(self.menu_items.values()) - self.menu_items[config.settings['default_online']].state = True - self.title = config.settings['default_online'] + self.menu_items[get_settings('Default Online')].state = True + self.title = get_settings('Default Online') self.icon = ICON rumps.Timer(self.update_menu, 5).start() + def add_settings_menu(self, name, options): + self.menu_items["Settings"].add(rumps.MenuItem(title=name, icon=None)) + selected_option = get_settings(name) + for option in options: + self.menu_items["Settings"][name].add( + rumps.MenuItem(title=option, callback=lambda sender: self.set_setting(sender, name), icon=None)) + if option == selected_option: + self.menu_items["Settings"][name][option].state = True + + def set_setting(self, sender, setting): + if sender.state: + return + + set_settings(setting, sender.title) + + for item in self.menu['Settings'][setting]: + self.menu_items['Settings'][setting][item].state = item == sender.title + + def update_menu(self, sender): for option in self.menu_items: + if not option in config.models: + continue if is_installing(option): self.menu_items[option].icon = ICON_INSTALLING elif is_installed(option): @@ -92,23 +135,26 @@ def update_menu(self, sender): currently_online = config.models[self.title]['type'] == 'remote' - if currently_online and config.settings['switch'] == 'automatic' or config.settings['switch'] == 'trigger_offline': + if currently_online and get_settings('Switch') == 'Automatic' or get_settings('Switch') == 'Trigger Offline': try: response = requests.get("http://google.com", timeout=1.0) if response.status_code != 200: - self.pick_model(self.menu_items[config.settings['default_offline']]) + self.pick_model(self.menu_items[get_settings('Default Offline')]) except requests.RequestException as e: - self.pick_model(self.menu_items[config.settings['default_offline']]) + self.pick_model(self.menu_items[get_settings('Default Offline')]) - if not currently_online and config.settings['switch'] == 'automatic': + if not currently_online and get_settings('Switch') == 'Automatic': try: response = requests.get("http://google.com", timeout=1.0) if response.status_code == 200: - self.pick_model(self.menu_items[config.settings['default_online']]) + self.pick_model(self.menu_items[get_settings('Default Online')]) except requests.RequestException as e: pass def pick_model(self, sender): + if (sender.state): + return + # check if the model is installed if is_installing(sender.title): rumps.alert("Model Installing", f"{sender.title} is currently installing.") @@ -122,13 +168,6 @@ def pick_model(self, sender): else: return - if (sender.state): - return - - # Toggle the checked status of the clicked menu item - sender.state = True - self.title = sender.title - # Send the choice to the local proxy app if sender.state: choice = sender.title @@ -142,13 +181,16 @@ def pick_model(self, sender): "Error", f"Failed to send selection. Server responded with: {response.status_code}.") except requests.RequestException as e: rumps.alert("Error", f"Failed to send selection. Error: {e}.") + return + + # Toggle the checked status of the clicked menu item + self.title = sender.title # If other options were previously selected, deselect them for item in self.menu: if item == 'Quit': continue - if item != sender.title: - self.menu_items[item].state = False + self.menu_items[item].state = item == sender.title def run_server(self): subprocess.run(['python', 'proxy.py']) diff --git a/config.py b/config.py index 337d345..b3833ae 100644 --- a/config.py +++ b/config.py @@ -22,13 +22,4 @@ } } -settings = { - 'default_online': 'GitHub', - 'default_offline': 'CodeLlama-7b', - # automatic -> switch to default_online if online, switch to default_offline if offline - # trigger_offline -> switch to default_offline if offline, don't switch if online - # manual -> don't switch - 'switch': 'automatic', -} - model_folder = os.path.expanduser('~/models') diff --git a/proxy.py b/proxy.py index eb83044..7c0884e 100644 --- a/proxy.py +++ b/proxy.py @@ -5,10 +5,16 @@ import logging from starlette import applications, responses, exceptions from starlette.requests import Request -import config +import json + +def get_settings(setting): + with open('settings.json', 'r') as f: + settings = json.load(f) + + return settings[setting] app = applications.Starlette() -state = config.models[config.settings['default_online']] +state = config.models[get_settings("Default Online")] local_server_process = None logging.basicConfig(level=logging.DEBUG) diff --git a/settings.json b/settings.json new file mode 100644 index 0000000..97dffb7 --- /dev/null +++ b/settings.json @@ -0,0 +1,5 @@ +{ + "Default Online": "GitHub", + "Default Offline": "CodeLlama-7b", + "Switch": "Automatic" +} \ No newline at end of file From 4621ac358a0c76b032c741b629bcfe61a651e3eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lyx=20Rothbo=CC=88ck?= Date: Sun, 25 Feb 2024 17:21:30 +0100 Subject: [PATCH 4/5] missing part of code Moved constants and setting functions to config; Added bool settings for various ui options; --- app.py | 145 ++++++++++++++++++++++++++++---------------------- config.py | 38 +++++++++++++ proxy.py | 9 +--- settings.json | 4 ++ 4 files changed, 124 insertions(+), 72 deletions(-) diff --git a/app.py b/app.py index 23e1940..9fb9335 100644 --- a/app.py +++ b/app.py @@ -1,4 +1,3 @@ -import json import rumps import requests import threading @@ -8,13 +7,6 @@ import config - -ICON = "resources/icon.png" -ICON_INSTALLED = "resources/installed.png" -ICON_INSTALLING = "resources/installing.png" -ICON_UNINSTALLED = "resources/uninstalled.png" - - def setup(): if not os.path.exists(config.model_folder): if input(f"Model folder {config.model_folder} does not exist. Create it? (y/n) ").lower() == 'y': @@ -64,90 +56,112 @@ def is_installing(model): return os.path.exists(f"{config.model_folder}/{model}.lock") else: return False - -def set_settings(setting, value): - with open('settings.json', 'r') as f: - settings = json.load(f) - - settings[setting] = value - - with open('settings.json', 'w') as f: - json.dump(settings, f, indent=4) - -def get_settings(setting): - with open('settings.json', 'r') as f: - settings = json.load(f) - - return settings[setting] class ModelPickerApp(rumps.App): def __init__(self): - super(ModelPickerApp, self).__init__("ModelPickerApp") + super(ModelPickerApp, self).__init__("ModelPickerApp", quit_button=None) + + self.rebuild_menu() + self.icon = config.ICON + rumps.Timer(self.update_menu, 5).start() - # Dynamically create menu items from the MENUBAR_OPTIONS + def rebuild_menu(self): + self.menu.clear() self.menu_items = {} + show_uninstalled = config.get_settings(config.SETTINGS_SHOW_UNINSTALLED) for option in config.models: + if not show_uninstalled and config.models[option]['type'] == 'local' and not is_installed(option): + continue self.menu_items[option] = rumps.MenuItem( title=option, callback=self.pick_model, icon=None) self.menu_items['Settings'] = rumps.MenuItem( title='Settings', icon=None) - self.add_settings_menu('Switch', ['Automatic', 'Trigger Offline', 'Manual']) - self.add_settings_menu('Default Online', filter(lambda x: config.models[x]['type'] == 'remote', config.models)) - self.add_settings_menu('Default Offline', filter(lambda x: config.models[x]['type'] == 'local', config.models)) - + self.add_bool_setting(config.SETTINGS_SHOW_UNINSTALLED, True) + self.add_bool_setting(config.SETTINGS_SHOW_STATUS_ICONS, True) + self.add_bool_setting(config.SETTINGS_SHOW_CURRENT_MODEL, True) + self.add_settings_menu(config.SETTINGS_SWITCH, ['Automatic', 'Trigger Offline', 'Manual']) + self.add_settings_menu(config.SETTINGS_DEFAULT_ONLINE, filter(lambda x: config.models[x]['type'] == 'remote', config.models)) + self.add_settings_menu(config.SETTINGS_DEFAULT_OFFLINE, filter(lambda x: config.models[x]['type'] == 'local' and (show_uninstalled or is_installed(x)), config.models)) + + self.menu_items['Quit'] = rumps.MenuItem(title='Quit', callback=rumps.quit_application, icon=None) + self.menu = list(self.menu_items.values()) - self.menu_items[get_settings('Default Online')].state = True - self.title = get_settings('Default Online') - self.icon = ICON - rumps.Timer(self.update_menu, 5).start() + self.menu_items[config.get_settings(config.SETTINGS_CURRENT_MODEL)].state = True + self.update_menu(None) + + if config.get_settings(config.SETTINGS_SHOW_CURRENT_MODEL): + self.title = config.get_settings(config.SETTINGS_CURRENT_MODEL) + else: + self.title = None - def add_settings_menu(self, name, options): + def add_settings_menu(self, name, options, triggerRebuild = False): self.menu_items["Settings"].add(rumps.MenuItem(title=name, icon=None)) - selected_option = get_settings(name) + selected_option = config.get_settings(name) + for option in options: self.menu_items["Settings"][name].add( - rumps.MenuItem(title=option, callback=lambda sender: self.set_setting(sender, name), icon=None)) + rumps.MenuItem(title=option, callback=lambda sender: self.set_setting(sender, name, triggerRebuild), icon=None)) if option == selected_option: self.menu_items["Settings"][name][option].state = True - def set_setting(self, sender, setting): + def add_bool_setting(self, name, triggerRebuild = False): + self.menu_items["Settings"].add( + rumps.MenuItem(title=name, callback=lambda sender: self.set_bool_setting(sender, name, triggerRebuild), icon=None)) + self.menu_items["Settings"][name].state = config.get_settings(name) + + def set_setting(self, sender, setting, triggerRebuild = False): if sender.state: return - set_settings(setting, sender.title) + config.set_settings(setting, sender.title) + + if triggerRebuild: + self.rebuild_menu() + else: + for item in self.menu['Settings'][setting]: + self.menu_items['Settings'][setting][item].state = item == sender.title + + def set_bool_setting(self, sender, setting, triggerRebuild = False): + config.set_settings(setting, not sender.state) - for item in self.menu['Settings'][setting]: - self.menu_items['Settings'][setting][item].state = item == sender.title + if triggerRebuild: + self.rebuild_menu() + else: + sender.state = config.get_settings(setting) def update_menu(self, sender): + status_icons = config.get_settings(config.SETTINGS_SHOW_STATUS_ICONS) for option in self.menu_items: if not option in config.models: continue - if is_installing(option): - self.menu_items[option].icon = ICON_INSTALLING - elif is_installed(option): - self.menu_items[option].icon = ICON_INSTALLED + if status_icons: + if is_installing(option): + self.menu_items[option].icon = config.ICON_INSTALLING + elif is_installed(option): + self.menu_items[option].icon = config.ICON_INSTALLED + else: + self.menu_items[option].icon = config.ICON_UNINSTALLED else: - self.menu_items[option].icon = ICON_UNINSTALLED + self.menu_items[option].icon = None - currently_online = config.models[self.title]['type'] == 'remote' + currently_online = config.models[config.get_settings(config.SETTINGS_CURRENT_MODEL)]['type'] == 'remote' - if currently_online and get_settings('Switch') == 'Automatic' or get_settings('Switch') == 'Trigger Offline': + if currently_online and config.get_settings(config.SETTINGS_SWITCH) == 'Automatic' or config.get_settings(config.SETTINGS_SWITCH) == 'Trigger Offline': try: response = requests.get("http://google.com", timeout=1.0) if response.status_code != 200: - self.pick_model(self.menu_items[get_settings('Default Offline')]) + self.pick_model(self.menu_items[config.get_settings(config.SETTINGS_DEFAULT_OFFLINE)]) except requests.RequestException as e: - self.pick_model(self.menu_items[get_settings('Default Offline')]) + self.pick_model(self.menu_items[config.get_settings(config.SETTINGS_DEFAULT_OFFLINE)]) - if not currently_online and get_settings('Switch') == 'Automatic': + if not currently_online and config.get_settings(config.SETTINGS_SWITCH) == 'Automatic': try: response = requests.get("http://google.com", timeout=1.0) if response.status_code == 200: - self.pick_model(self.menu_items[get_settings('Default Online')]) + self.pick_model(self.menu_items[config.get_settings(config.SETTINGS_DEFAULT_ONLINE)]) except requests.RequestException as e: pass @@ -169,22 +183,23 @@ def pick_model(self, sender): return # Send the choice to the local proxy app - if sender.state: - choice = sender.title - try: - response = requests.post( - "http://localhost:5001/set_target", json={"target": choice}, timeout=1.0) - if response.status_code == 200: - print(f"Successfully sent selection: {choice}.") - else: - rumps.alert( - "Error", f"Failed to send selection. Server responded with: {response.status_code}.") - except requests.RequestException as e: - rumps.alert("Error", f"Failed to send selection. Error: {e}.") - return + choice = sender.title + try: + response = requests.post( + "http://localhost:5001/set_target", json={"target": choice}, timeout=1.0) + if response.status_code == 200: + print(f"Successfully sent selection: {choice}.") + else: + rumps.alert( + "Error", f"Failed to send selection. Server responded with: {response.status_code}.") + except requests.RequestException as e: + rumps.alert("Error", f"Failed to send selection. Error: {e}.") + return # Toggle the checked status of the clicked menu item - self.title = sender.title + if config.get_settings(config.SETTINGS_SHOW_CURRENT_MODEL): + self.title = sender.title + config.set_settings(config.SETTINGS_CURRENT_MODEL, sender.title) # If other options were previously selected, deselect them for item in self.menu: diff --git a/config.py b/config.py index b3833ae..7c154d0 100644 --- a/config.py +++ b/config.py @@ -1,4 +1,22 @@ import os +import json + +ICON = "resources/icon.png" +ICON_INSTALLED = "resources/installed.png" +ICON_INSTALLING = "resources/installing.png" +ICON_UNINSTALLED = "resources/uninstalled.png" + +SETTINGS_CURRENT_MODEL = "Current Model" +SETTINGS_SHOW_UNINSTALLED = "Show Uninstalled" +SETTINGS_SHOW_STATUS_ICONS = "Show Status Icons" +SETTINGS_SHOW_CURRENT_MODEL = "Show Current Model" +SETTINGS_DEFAULT_ONLINE = "Default Online" +SETTINGS_DEFAULT_OFFLINE = "Default Offline" +SETTINGS_SWITCH = "Switch" + +SWITCH_AUTOMATIC = "Automatic" +SWITCH_TRIGGER_OFFLINE = "Trigger Offline" +SWITCH_MANUAL = "Manual" models = { 'GitHub': { @@ -15,6 +33,11 @@ 'type': 'local', 'filename': 'mistral-7b-instruct-v0.1.Q5_K_M.gguf', }, + "stable-code-3b": { + "url": "https://huggingface.co/stabilityai/stable-code-3b/resolve/main/stable-code-3b-Q5_K_M.gguf", + "type": "local", + "filename": "stable-code-3b-Q5_K_M.gguf", + }, 'CodeLlama-34b': { 'url': 'https://huggingface.co/TheBloke/CodeLlama-34B-Instruct-GGUF/resolve/main/codellama-34b-instruct.Q4_K_M.gguf', 'type': 'local', @@ -23,3 +46,18 @@ } model_folder = os.path.expanduser('~/models') + +def set_settings(setting, value): + with open('settings.json', 'r') as f: + settings = json.load(f) + + settings[setting] = value + + with open('settings.json', 'w') as f: + json.dump(settings, f, indent=4) + +def get_settings(setting): + with open('settings.json', 'r') as f: + settings = json.load(f) + + return settings[setting] diff --git a/proxy.py b/proxy.py index 7c0884e..3ef0b51 100644 --- a/proxy.py +++ b/proxy.py @@ -5,16 +5,11 @@ import logging from starlette import applications, responses, exceptions from starlette.requests import Request -import json -def get_settings(setting): - with open('settings.json', 'r') as f: - settings = json.load(f) - - return settings[setting] +import config app = applications.Starlette() -state = config.models[get_settings("Default Online")] +state = config.models[config.get_settings(config.SETTINGS_DEFAULT_ONLINE)] local_server_process = None logging.basicConfig(level=logging.DEBUG) diff --git a/settings.json b/settings.json index 97dffb7..114b19b 100644 --- a/settings.json +++ b/settings.json @@ -1,4 +1,8 @@ { + "Current Model": "GitHub", + "Show Uninstalled": false, + "Show Status Icons": true, + "Show Current Model": true, "Default Online": "GitHub", "Default Offline": "CodeLlama-7b", "Switch": "Automatic" From e9deb0868a88155ad5323b334d2c228100c64df1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lyx=20Rothbo=CC=88ck?= Date: Sun, 25 Feb 2024 19:19:00 +0100 Subject: [PATCH 5/5] Also kill 8000 --- proxy.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/proxy.py b/proxy.py index 3ef0b51..5d16843 100644 --- a/proxy.py +++ b/proxy.py @@ -89,14 +89,17 @@ async def server_error(request, exc): import uvicorn import psutil - # kill any existing local server on 5001 + # kill any existing local server on 5001 or 8000 for proc in psutil.process_iter(): try: for conns in proc.connections(kind='inet'): if conns.laddr.port == 5001: print(f"Killing process {proc.name()} on port 5001") proc.kill() + if conns.laddr.port == 8000: + print(f"Killing process {proc.name()} on port 8000") + proc.kill() except: - pass + continue uvicorn.run(app, host="0.0.0.0", port=5001)