Added support for background pulling of models

This commit is contained in:
jeffser 2024-05-19 17:09:58 -06:00
parent 454aeac5e2
commit d540114ae5
2 changed files with 88 additions and 95 deletions

View File

@ -21,7 +21,7 @@ import gi
gi.require_version('GtkSource', '5') gi.require_version('GtkSource', '5')
gi.require_version('GdkPixbuf', '2.0') gi.require_version('GdkPixbuf', '2.0')
from gi.repository import Adw, Gtk, Gdk, GLib, GtkSource, Gio, GdkPixbuf from gi.repository import Adw, Gtk, Gdk, GLib, GtkSource, Gio, GdkPixbuf
import json, requests, threading, os, re, base64 import json, requests, threading, os, re, base64, sys
from io import BytesIO from io import BytesIO
from PIL import Image from PIL import Image
from datetime import datetime from datetime import datetime
@ -35,6 +35,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
#Variables #Variables
ollama_url = None ollama_url = None
local_models = [] local_models = []
pulling_models = {}
chats = {"chats": {"New Chat": {"messages": []}}, "selected_chat": "New Chat"} chats = {"chats": {"New Chat": {"messages": []}}, "selected_chat": "New Chat"}
attached_image = {"path": None, "base64": None} attached_image = {"path": None, "base64": None}
first_time_setup = False first_time_setup = False
@ -50,7 +51,6 @@ class AlpacaWindow(Adw.ApplicationWindow):
connection_next_button = Gtk.Template.Child() connection_next_button = Gtk.Template.Child()
connection_url_entry = Gtk.Template.Child() connection_url_entry = Gtk.Template.Child()
main_overlay = Gtk.Template.Child() main_overlay = Gtk.Template.Child()
pull_overlay = Gtk.Template.Child()
manage_models_overlay = Gtk.Template.Child() manage_models_overlay = Gtk.Template.Child()
connection_overlay = Gtk.Template.Child() connection_overlay = Gtk.Template.Child()
chat_container = Gtk.Template.Child() chat_container = Gtk.Template.Child()
@ -64,10 +64,10 @@ class AlpacaWindow(Adw.ApplicationWindow):
manage_models_button = Gtk.Template.Child() manage_models_button = Gtk.Template.Child()
manage_models_dialog = Gtk.Template.Child() manage_models_dialog = Gtk.Template.Child()
available_model_list_box = Gtk.Template.Child() pulling_model_list_box = Gtk.Template.Child()
local_model_list_box = Gtk.Template.Child() local_model_list_box = Gtk.Template.Child()
available_model_list_box = Gtk.Template.Child()
pull_model_dialog = Gtk.Template.Child()
pull_model_status_page = Gtk.Template.Child() pull_model_status_page = Gtk.Template.Child()
pull_model_progress_bar = Gtk.Template.Child() pull_model_progress_bar = Gtk.Template.Child()
@ -180,7 +180,22 @@ class AlpacaWindow(Adw.ApplicationWindow):
for i in range(self.model_string_list.get_n_items() -1, -1, -1): for i in range(self.model_string_list.get_n_items() -1, -1, -1):
self.model_string_list.remove(i) self.model_string_list.remove(i)
if response['status'] == 'ok': if response['status'] == 'ok':
self.local_model_list_box.remove_all()
for model in json.loads(response['text'])['models']: for model in json.loads(response['text'])['models']:
model_row = Adw.ActionRow(
title = model["name"].split(":")[0],
subtitle = model["name"].split(":")[1]
)
button = Gtk.Button(
icon_name = "user-trash-symbolic",
vexpand = False,
valign = 3,
css_classes = ["error"]
)
button.connect("clicked", lambda button=button, model_name=model["name"]: self.model_delete_button_activate(model_name))
model_row.add_suffix(button)
self.local_model_list_box.append(model_row)
self.model_string_list.append(model["name"]) self.model_string_list.append(model["name"])
self.local_models.append(model["name"]) self.local_models.append(model["name"])
self.model_drop_down.set_selected(0) self.model_drop_down.set_selected(0)
@ -350,7 +365,6 @@ class AlpacaWindow(Adw.ApplicationWindow):
if dialog.choose_finish(task) == "delete": if dialog.choose_finish(task) == "delete":
response = simple_delete(self.ollama_url + "/api/delete", data={"name": model_name}) response = simple_delete(self.ollama_url + "/api/delete", data={"name": model_name})
self.update_list_local_models() self.update_list_local_models()
self.update_list_available_models()
if response['status'] == 'ok': if response['status'] == 'ok':
self.show_toast("good", 0, self.manage_models_overlay) self.show_toast("good", 0, self.manage_models_overlay)
else: else:
@ -358,59 +372,71 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.manage_models_dialog.close() self.manage_models_dialog.close()
self.show_connection_dialog(True) self.show_connection_dialog(True)
def pull_model_update(self, data): def pull_model_update(self, data, model_name):
try: if model_name in list(self.pulling_models.keys()):
GLib.idle_add(self.pull_model_progress_bar.set_text, data['status']) GLib.idle_add(self.pulling_models[model_name].set_subtitle, data['status'] + (f" | {round(data['completed'] / data['total'] * 100, 2)}%" if 'completed' in data and 'total' in data else ""))
if 'completed' in data: else:
if 'total' in data: GLib.idle_add(self.pull_model_progress_bar.set_fraction, data['completed'] / data['total']) sys.exit()
else: GLib.idle_add(self.pull_model_progress_bar.set_fraction, 1.0)
else:
GLib.idle_add(self.pull_model_progress_bar.set_fraction, 0.0)
except Exception as e: print(e)
def pull_model(self, dialog, task, model_name, tag): def pull_model(self, model_name, tag):
if dialog.choose_finish(task) == "pull": data = {"name":f"{model_name}:{tag}"}
data = {"name":f"{model_name}:{tag}"} response = stream_post(f"{self.ollama_url}/api/pull", data=json.dumps(data), callback=lambda data, model_name=f"{model_name}:{tag}": self.pull_model_update(data, model_name))
GLib.idle_add(self.update_list_local_models)
if response['status'] == 'ok':
GLib.idle_add(self.show_notification, "Task Complete", f"Model '{model_name}:{tag}' pulled successfully.", True, Gio.ThemedIcon.new("emblem-ok-symbolic"))
GLib.idle_add(self.show_toast, "good", 1, self.manage_models_overlay)
GLib.idle_add(self.pulling_models[f"{model_name}:{tag}"].get_parent().remove, self.pulling_models[f"{model_name}:{tag}"])
del self.pulling_models[f"{model_name}:{tag}"]
GLib.idle_add(self.pull_model_dialog.present, self.manage_models_dialog) else:
response = stream_post(f"{self.ollama_url}/api/pull", data=json.dumps(data), callback=self.pull_model_update) GLib.idle_add(self.show_notification, "Pull Model Error", f"Failed to pull model '{model_name}:{tag}' due to network error.", True, Gio.ThemedIcon.new("dialog-error-symbolic"))
GLib.idle_add(self.show_toast, "error", 4, self.connection_overlay)
GLib.idle_add(self.manage_models_dialog.close)
GLib.idle_add(self.show_connection_dialog, True)
GLib.idle_add(self.update_list_local_models) def stop_pull_model(self, dialog, task, model_name):
GLib.idle_add(self.update_list_available_models) if dialog.choose_finish(task) == "stop":
GLib.idle_add(self.pull_model_dialog.force_close) GLib.idle_add(self.pulling_models[model_name].get_parent().remove, self.pulling_models[model_name])
if response['status'] == 'ok': del self.pulling_models[model_name]
GLib.idle_add(self.show_notification, "Task Complete", f"Model '{model_name}:{tag}' pulled successfully.", True, Gio.ThemedIcon.new("emblem-ok-symbolic"))
GLib.idle_add(self.show_toast, "good", 1, self.manage_models_overlay)
else:
GLib.idle_add(self.show_notification, "Pull Model Error", f"Failed to pull model '{model_name}:{tag}' due to network error.", True, Gio.ThemedIcon.new("dialog-error-symbolic"))
GLib.idle_add(self.show_toast, "error", 4, self.connection_overlay)
GLib.idle_add(self.manage_models_dialog.close)
GLib.idle_add(self.show_connection_dialog, True)
def stop_pull_model_dialog(self, model_name):
def pull_model_start(self, dialog, task, model_name, tag_drop_down):
tag = tag_drop_down.get_selected_item().get_string()
self.pull_model_status_page.set_description(f"{model_name}:{tag}")
thread = threading.Thread(target=self.pull_model, args=(dialog, task, model_name, tag))
thread.start()
def model_action_button_activate(self, button, model_name):
action = list(set(button.get_css_classes()) & set(["delete", "pull"]))[0]
dialog = Adw.AlertDialog( dialog = Adw.AlertDialog(
heading=f"{action.capitalize()} Model", heading="Stop Model",
body=f"Are you sure you want to {action} '{model_name}'?", body=f"Are you sure you want to stop pulling '{model_name}'?",
close_response="cancel" close_response="cancel"
) )
dialog.add_response("cancel", "Cancel") dialog.add_response("cancel", "Cancel")
dialog.add_response(action, action.capitalize()) dialog.add_response("stop", "Stop")
dialog.set_response_appearance(action, Adw.ResponseAppearance.DESTRUCTIVE if action == "delete" else Adw.ResponseAppearance.SUGGESTED) dialog.set_response_appearance("stop", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.choose( dialog.choose(
parent = self.manage_models_dialog, parent = self.manage_models_dialog,
cancellable = None, cancellable = None,
callback = lambda dialog, task, model_name = model_name, button = button: callback = lambda dialog, task, model_name = model_name: self.stop_pull_model(dialog, task, model_name)
self.delete_model(dialog, task, model_name, button) if action == "delete" else self.pull_model_start(dialog, task, model_name,button)
) )
def pull_model_start(self, dialog, task, model_name, tag_drop_down):
if dialog.choose_finish(task) == "pull":
tag = tag_drop_down.get_selected_item().get_string()
if f"{model_name}:{tag}" in list(self.pulling_models.keys()): return ##TODO add message: 'already being pulled'
if f"{model_name}:{tag}" in self.local_models: return ##TODO add message 'already pulled'
#self.pull_model_status_page.set_description(f"{model_name}:{tag}")
model_row = Adw.ActionRow(
title = f"{model_name}:{tag}",
subtitle = ""
)
thread = threading.Thread(target=self.pull_model, args=(model_name, tag))
self.pulling_models[f"{model_name}:{tag}"] = model_row
button = Gtk.Button(
icon_name = "media-playback-stop-symbolic",
vexpand = False,
valign = 3,
css_classes = ["error"]
)
button.connect("clicked", lambda button, model_name=f"{model_name}:{tag}" : self.stop_pull_model_dialog(model_name))
model_row.add_suffix(button)
self.pulling_model_list_box.append(model_row)
thread.start()
def model_delete_button_activate(self, model_name): def model_delete_button_activate(self, model_name):
dialog = Adw.AlertDialog( dialog = Adw.AlertDialog(
heading="Delete Model", heading="Delete Model",
@ -450,23 +476,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
) )
def update_list_available_models(self): def update_list_available_models(self):
self.local_model_list_box.remove_all()
self.available_model_list_box.remove_all() self.available_model_list_box.remove_all()
for model_name in self.local_models:
model = Adw.ActionRow(
title = model_name.split(":")[0],
subtitle = model_name.split(":")[1]
)
button = Gtk.Button(
icon_name = "user-trash-symbolic",
vexpand = False,
valign = 3,
css_classes = ["error"]
)
button.connect("clicked", lambda button=button, model_name=model_name: self.model_delete_button_activate(model_name))
model.add_suffix(button)
self.local_model_list_box.append(model)
for name, model_info in available_models.items(): for name, model_info in available_models.items():
model = Adw.ActionRow( model = Adw.ActionRow(
title = name, title = name,
@ -484,7 +494,6 @@ class AlpacaWindow(Adw.ApplicationWindow):
def manage_models_button_activate(self, button=None): def manage_models_button_activate(self, button=None):
self.update_list_local_models() self.update_list_local_models()
self.update_list_available_models()
self.manage_models_dialog.present(self) self.manage_models_dialog.present(self)
def connection_carousel_page_changed(self, carousel, index): def connection_carousel_page_changed(self, carousel, index):
@ -785,9 +794,6 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.model_drop_down.set_selected(i) self.model_drop_down.set_selected(i)
break break
def selected_model_changed(self, pspec=None, user_data=None):
self.verify_if_image_can_be_used()
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
GtkSource.init() GtkSource.init()
@ -801,7 +807,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.image_button.connect("clicked", self.open_image) self.image_button.connect("clicked", self.open_image)
self.add_chat_button.connect("clicked", lambda button : self.chat_new_dialog("Enter name for new chat", False)) self.add_chat_button.connect("clicked", lambda button : self.chat_new_dialog("Enter name for new chat", False))
self.set_default_widget(self.send_button) self.set_default_widget(self.send_button)
self.model_drop_down.connect("notify", self.selected_model_changed) self.model_drop_down.connect("notify", self.verify_if_image_can_be_used)
self.chat_list_box.connect("row-selected", self.chat_changed) self.chat_list_box.connect("row-selected", self.chat_changed)
#self.message_text_view.set_activates_default(self.send_button) #self.message_text_view.set_activates_default(self.send_button)
self.connection_carousel.connect("page-changed", self.connection_carousel_page_changed) self.connection_carousel.connect("page-changed", self.connection_carousel_page_changed)
@ -817,4 +823,5 @@ class AlpacaWindow(Adw.ApplicationWindow):
else: else:
self.first_time_setup = True self.first_time_setup = True
self.connection_dialog.present(self) self.connection_dialog.present(self)
self.update_list_available_models()
self.update_chat_list() self.update_chat_list()

View File

@ -245,35 +245,6 @@
</object> </object>
</property> </property>
<object class="AdwDialog" id="pull_model_dialog">
<property name="can-close">false</property>
<property name="width-request">400</property>
<child>
<object class="AdwToastOverlay" id="pull_overlay">
<child>
<object class="AdwToolbarView">
<child>
<object class="AdwStatusPage" id="pull_model_status_page">
<property name="hexpand">true</property>
<property name="vexpand">true</property>
<property name="margin-top">24</property>
<property name="margin-bottom">24</property>
<property name="margin-start">24</property>
<property name="margin-end">24</property>
<property name="title" translatable="yes">Pulling Model</property>
<child>
<object class="GtkProgressBar" id="pull_model_progress_bar">
<property name="show-text">true</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
<object class="AdwDialog" id="manage_models_dialog"> <object class="AdwDialog" id="manage_models_dialog">
<property name="can-close">true</property> <property name="can-close">true</property>
<property name="width-request">400</property> <property name="width-request">400</property>
@ -303,6 +274,21 @@
<object class="GtkBox"> <object class="GtkBox">
<property name="orientation">1</property> <property name="orientation">1</property>
<property name="spacing">12</property> <property name="spacing">12</property>
<child>
<object class="GtkListBox" id="pulling_model_list_box">
<property name="selection-mode">none</property>
<style>
<class name="boxed-list"/>
</style>
</object>
</child>
<child>
<object class="GtkSeparator">
<style>
<class name="spacer"/>
</style>
</object>
</child>
<child> <child>
<object class="GtkListBox" id="local_model_list_box"> <object class="GtkListBox" id="local_model_list_box">
<property name="selection-mode">none</property> <property name="selection-mode">none</property>