diff --git a/README.md b/README.md deleted file mode 100644 index 2cfd3d5..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# PyLlama - diff --git a/README.org b/README.org new file mode 100644 index 0000000..4f722c7 --- /dev/null +++ b/README.org @@ -0,0 +1,4 @@ +#+TITLE: PyLlama + +*PyLlama* is a simple frontend to *ollama*, designed to run on old +PCs, which other frontends don't like. diff --git a/main.py b/main.py new file mode 100644 index 0000000..6c3efe9 --- /dev/null +++ b/main.py @@ -0,0 +1,121 @@ +import gi +gi.require_version("Gtk", "4.0") +from gi.repository import GLib, Gtk + +import ollama + +def set_margin_all(widget, m): + widget.set_margin_start(m) + widget.set_margin_top(m) + widget.set_margin_end(m) + widget.set_margin_bottom(m) + +class PyLlamaWindow(Gtk.ApplicationWindow): + def __init__(self, application=None, title=None): + super().__init__(application=application, title=title) + self.set_default_size(800, 600) + self.cur_ai_response = None + + self.model_store = Gtk.StringList() + self.model_dropdown = Gtk.DropDown() + self.model_dropdown.set_model(self.model_store) + self.model_dropdown.set_tooltip_text("Select model") + self.model_dropdown.set_enable_search(True) + + header = Gtk.HeaderBar() + header.set_show_title_buttons(True) + header.set_title_widget(self.model_dropdown) + + self.set_titlebar(header) + + vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 6) + set_margin_all(vbox, 8) + + self.listbox = Gtk.ListBox.new() + self.listbox.set_selection_mode(Gtk.SelectionMode.NONE) + + scrolled = Gtk.ScrolledWindow() + scrolled.set_vexpand(True) + scrolled.set_child(self.listbox) + vbox.append(scrolled) + + entry = Gtk.Entry() + entry_buffer = entry.get_buffer() + entry.set_placeholder_text("Type message and press Enter...") + entry.set_hexpand(True) + entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, "go-next-symbolic") + entry.connect("activate", self.send_message) + entry.connect("icon_press", self.send_message) + + vbox.append(entry) + self.set_child(vbox) + + self.current_model = None + self.populate_models() + + def populate_models(self): + try: + models = ollama.list() + except Exception as e: + models = [] + print ("Failed to fetch models: ", e) + + names = [m.model for m in models["models"]] + self.model_store.splice(0, len(list(self.model_store)), names) + + def add_message(self, text=None, sender=True): + label = Gtk.Label() + label.set_selectable(True) + label.set_wrap(True) + label.set_xalign(1.0 if sender else 0.0) + label.set_text("" if not text else text) + set_margin_all(label, 8) + + row = Gtk.ListBoxRow() + row.set_child(label) + self.listbox.append(row) + + adj = self.listbox.get_adjustment() + adj.set_value(adj.get_upper()) + + return label + + def add_response_part(self, part): + if self.cur_ai_response == None: + return + + self.cur_ai_response.set_label(self.cur_ai_response.get_label() + part) + + def send_message(self, entry, _=None): + buffer = entry.get_buffer() + text = buffer.get_text() + if text == None or text == "": + return + + buffer.delete_text(0, -1) # Delete all text + self.add_message(text=text) + self.cur_ai_response = self.add_message(text=None, sender=False) + self.get_application().send_prompt(self.model_dropdown.get_selected_item().get_string(), text) + +class PyLlama(Gtk.Application): + def __init__(self): + super().__init__(application_id="eur.cfpi-fpsi.pyllama") + GLib.set_application_name("PyLlama") + + def do_activate(self): + self.client = ollama.Client() + self.window = PyLlamaWindow(application=self, title="PyLlama") + self.window.present() + + def send_prompt(self, model, text): + messages = [{ + "role": "user", + "content": text + }] + + for part in self.client.chat(model, messages=messages, stream=True): + self.window.add_response_part(part.message.content) + +if __name__ == "__main__": + app = PyLlama() + app.run() diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..8a597df --- /dev/null +++ b/shell.nix @@ -0,0 +1,13 @@ +let + pkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-25.11") {}; +in pkgs.mkShell { + packages = with pkgs; [ + gtk4 + gobject-introspection + + (python3.withPackages (p: with p; [ + ollama + pygobject3 + ])) + ]; +}