From 174673d337105312c9f8ac789de45a384c163d1b Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 4 Jul 2024 15:29:33 +1200 Subject: [PATCH] feat: add edit model cli and tui (#64) --- README.md | 52 ++++++++++++++++++++++++--------- app_model.go | 25 +++++++++------- keymap.go | 4 +-- main.go | 15 ++++++++-- operations.go | 79 +++++++++++++++++++++++++++++++++++++++++++++++---- 5 files changed, 141 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index a0313f0..b4af90d 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ The application allows users to interactively select models, sort them by variou - [Installation](#installation) - [Usage](#usage) - [Key Bindings](#key-bindings) + - [Top](#top) + - [Inspect](#inspect) - [Command-line Options](#command-line-options) - [Configuration](#configuration) - [Installation and build from source](#installation-and-build-from-source) @@ -76,30 +78,43 @@ echo "alias g=gollama" >> ~/.zshrc - `i`: Inspect model - `t`: Top (show running models) _**(Work in progress)**_ - `D`: Delete model +- `e`: Edit model **new** - `c`: Copy model -- `r`: Rename model _**(Work in progress)**_ -- `u`: Update model (edit Modelfile) _**(Work in progress)**_ - `U`: Unload all models - `P`: Push model - `n`: Sort by name - `s`: Sort by size - `m`: Sort by modified -- `k`: Sort by quantization +- `k`: Sort by quantisation - `f`: Sort by family - `l`: Link model to LM Studio - `L`: Link all models to LM Studio +- `r`: Rename model _**(Work in progress)**_ - `q`: Quit +#### Top + +Top (`t`) + +![](screenshots/gollama-top.jpg) + +#### Inspect + +Inspect (`i`) + +![](screenshots/gollama-inspect.png) + #### Command-line Options - `-l`: List all available Ollama models and exit -- `-ollama-dir`: Custom Ollama models directory -- `-lm-dir`: Custom LM Studio models directory -- `-no-cleanup`: Don't cleanup broken symlinks -- `-s `: Search for models by name +- `-s `: Search for models by name **new** - OR operator (`'term1|term2'`) returns models that match either term - AND operator (`'term1&term2'`) returns models that match both terms +- `-e `: Edit the Modelfile for a model **new** +- `-ollama-dir`: Custom Ollama models directory +- `-lm-dir`: Custom LM Studio models directory - `-cleanup`: Remove all symlinked models and empty directories and exit +- `-no-cleanup`: Don't cleanup broken symlinks - `-u`: Unload all running models - `-v`: Print the version and exit @@ -115,17 +130,25 @@ List (`gollama -l`): ![](screenshots/cli-list.jpg) -##### Inspect +##### Edit -Inspect (`i`) +Gollama can be called with `-e` to edit the Modelfile for a model. -![](screenshots/gollama-inspect.png) +```shell +gollama -e my-model +``` -##### Top +##### Search -Top (`t`) +Gollama can be called with `-s` to search for models by name. -![](screenshots/gollama-top.jpg) +```shell +gollama -s my-model # returns models that contain 'my-model' + +gollama -s 'my-model|my-other-model' # returns models that contain either 'my-model' or 'my-other-model' + +gollama -s 'my-model&instruct' # returns models that contain both 'my-model' and 'instruct' +``` ## Configuration @@ -228,9 +251,10 @@ Please fork the repository and create a pull request with your changes. - [Llama.cpp](https://github.com/ggerganov/llama.cpp) - [Charmbracelet](https://charm.sh/) -Thank you to folks such as Matt Williams for giving this a shot and providing feedback. +Thank you to folks such as Matt Williams and Fahd Mirza for giving this a shot and providing feedback. [![Matt Williams - My favourite way to run Ollama: Gollama](https://img.youtube.com/vi/OCXuYm6LKgE/0.jpg)](https://www.youtube.com/watch?v=OCXuYm6LKgE) +[![Fahd Mirza - Gollama - Manage Ollama Models Locally](https://img.youtube.com/vi/24yqFrQV-4Q/0.jpg)](https://www.youtube.com/watch?v=24yqFrQV-4Q) ## License diff --git a/app_model.go b/app_model.go index 69d025b..f623b26 100644 --- a/app_model.go +++ b/app_model.go @@ -181,7 +181,7 @@ func (m *AppModel) handleKeyMsg(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m.handleAltScreenKey() case key.Matches(msg, m.keys.ClearScreen): return m.handleClearScreenKey() - case key.Matches(msg, m.keys.UpdateModel): + case key.Matches(msg, m.keys.EditModel): return m.handleUpdateModelKey() case key.Matches(msg, m.keys.UnloadModels): return m.handleUnloadModelsKey() @@ -426,18 +426,19 @@ func (m *AppModel) handleTopKey() (tea.Model, tea.Cmd) { func (m *AppModel) handleUpdateModelKey() (tea.Model, tea.Cmd) { logging.DebugLogger.Println("UpdateModel key matched") - defer func() { - m.refreshList() - }() if item, ok := m.list.SelectedItem().(Model); ok { m.editing = true - modelfilePath, err := copyModelfile(item.Name, item.Name, m.client) + message, err := editModelfile(m.client, item.Name) if err != nil { - m.message = fmt.Sprintf("Error copying modelfile: %v", err) - return m, nil + m.message = fmt.Sprintf("Error updating model: %v", err) + } else { + m.message = message } - return m, openEditor(modelfilePath) + m.clearScreen() + m.refreshList() + return m, nil } + m.refreshList() return m, nil } @@ -603,7 +604,11 @@ func (m *AppModel) View() string { return m.filterView() } - view := m.list.View() + "\n" + m.message + view := m.list.View() + + if m.message != "" { + view += "\n\n" + lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Render(m.message) + } if m.showProgress { view += "\n" + m.progress.View() @@ -756,7 +761,7 @@ func (k KeyMap) FullHelp() [][]key.Binding { return [][]key.Binding{ {k.Space, k.Delete, k.RunModel, k.LinkModel, k.LinkAllModels, k.CopyModel, k.PushModel}, // first column {k.SortByName, k.SortBySize, k.SortByModified, k.SortByQuant, k.SortByFamily}, // second column - {k.Top, k.UpdateModel, k.InspectModel, k.Quit}, // third column + {k.Top, k.EditModel, k.InspectModel, k.Quit}, // third column } } diff --git a/keymap.go b/keymap.go index d45cf4d..43014a3 100644 --- a/keymap.go +++ b/keymap.go @@ -25,7 +25,7 @@ type KeyMap struct { PushModel key.Binding Top key.Binding AltScreen key.Binding - UpdateModel key.Binding + EditModel key.Binding UnloadModels key.Binding Help key.Binding RenameModel key.Binding @@ -48,6 +48,7 @@ func NewKeyMap() *KeyMap { Delete: key.NewBinding(key.WithKeys("D"), key.WithHelp("D", "delete")), Help: key.NewBinding(key.WithKeys("h"), key.WithHelp("h", "help")), InspectModel: key.NewBinding(key.WithKeys("i"), key.WithHelp("i", "inspect")), + EditModel: key.NewBinding(key.WithKeys("e"), key.WithHelp("e", "edit model")), LinkAllModels: key.NewBinding(key.WithKeys("L"), key.WithHelp("L", "link all")), LinkModel: key.NewBinding(key.WithKeys("l"), key.WithHelp("l", "link (L=all)")), PushModel: key.NewBinding(key.WithKeys("P"), key.WithHelp("P", "push")), @@ -59,7 +60,6 @@ func NewKeyMap() *KeyMap { SortByQuant: key.NewBinding(key.WithKeys("k"), key.WithHelp("k", "^quant")), SortBySize: key.NewBinding(key.WithKeys("s"), key.WithHelp("s", "^size")), Top: key.NewBinding(key.WithKeys("t"), key.WithHelp("t", "top")), - UpdateModel: key.NewBinding(key.WithKeys("u"), key.WithHelp("u", "update model")), UnloadModels: key.NewBinding(key.WithKeys("U"), key.WithHelp("U", "unload all")), } } diff --git a/main.go b/main.go index 17ccd9f..f1de4b7 100644 --- a/main.go +++ b/main.go @@ -73,7 +73,7 @@ var Version string // Version is set by the build system func main() { if Version == "" { - Version = "1.13.1" + Version = "1.16.0" } cfg, err := config.LoadConfig() @@ -97,6 +97,7 @@ func main() { unloadModelsFlag := flag.Bool("u", false, "Unload all models and exit") versionFlag := flag.Bool("v", false, "Print the version and exit") hostFlag := flag.String("h", "", "Override the config file to set the Ollama API host (e.g. http://localhost:11434)") + editFlag := flag.Bool("e", false, "Edit a model's modelfile") flag.Parse() @@ -248,6 +249,16 @@ func main() { os.Exit(0) } + if *editFlag { + if flag.NArg() == 0 { + fmt.Println("Usage: gollama -e ") + os.Exit(1) + } + modelName := flag.Args()[0] + editModelfile(client, modelName) + os.Exit(0) + } + l := list.New(items, NewItemDelegate(&app), width, height-5) l.Title = "Ollama Models" l.Help.Styles.ShortDesc.Bold(true) @@ -276,7 +287,7 @@ func main() { keys.CopyModel, keys.PushModel, keys.Top, - keys.UpdateModel, + keys.EditModel, keys.Help, } } diff --git a/operations.go b/operations.go index 6557320..f1a76ef 100644 --- a/operations.go +++ b/operations.go @@ -28,14 +28,14 @@ func runModel(model string, cfg *config.Config) tea.Cmd { ollamaPath, err := exec.LookPath("ollama") if err != nil { - logging.ErrorLogger.Printf("Error finding ollama binary: %v\n", err) + logging.ErrorLogger.Printf("error finding ollama binary: %v\n", err) logging.ErrorLogger.Printf("If you're running Ollama in a container, make sure you updated the config file with the container name\n") return nil } c := exec.Command(ollamaPath, "run", model) return tea.ExecProcess(c, func(err error) tea.Msg { if err != nil { - logging.ErrorLogger.Printf("Error running model: %v\n", err) + logging.ErrorLogger.Printf("error running model: %v\n", err) } return runFinishedMessage{err} }) @@ -44,7 +44,7 @@ func runModel(model string, cfg *config.Config) tea.Cmd { func runDocker(container string, model string) tea.Cmd { dockerPath, err := exec.LookPath("docker") if err != nil { - logging.ErrorLogger.Printf("Error finding docker binary: %v\n", err) + logging.ErrorLogger.Printf("error finding docker binary: %v\n", err) return nil } @@ -54,7 +54,7 @@ func runDocker(container string, model string) tea.Cmd { c := exec.Command(dockerPath, args...) return tea.ExecProcess(c, func(err error) tea.Msg { if err != nil { - logging.ErrorLogger.Printf("Error running model in docker container: %v\n", err) + logging.ErrorLogger.Printf("error running model in docker container: %v\n", err) } return runFinishedMessage{err} }) @@ -581,8 +581,6 @@ func openEditor(filePath string) tea.Cmd { } func createModelFromModelfile(modelName, modelfilePath string, client *api.Client) error { - // cmd := exec.Command("ollama", "create", "-f", modelfilePath, modelName) - // return cmd.Run() ctx := context.Background() req := &api.CreateRequest{ Name: modelName, @@ -651,3 +649,72 @@ func unloadModel(client *api.Client, modelName string) (string, error) { return modelName, nil } + +func editModelfile(client *api.Client, modelName string) (string, error) { + if client == nil { + return "", fmt.Errorf("error: Client is nil") + } + ctx := context.Background() + + // Fetch the current modelfile from the server + showResp, err := client.Show(ctx, &api.ShowRequest{Name: modelName}) + if err != nil { + return "", fmt.Errorf("error fetching modelfile for %s: %v", modelName, err) + } + modelfileContent := showResp.Modelfile + + if os.Getenv("EDITOR") == "" { + os.Setenv("EDITOR", "vim") + } + + logging.DebugLogger.Printf("Editing modelfile for model: %s\n", modelName) + + // Write the fetched content to a temporary file + tempDir := os.TempDir() + newModelfilePath := filepath.Join(tempDir, fmt.Sprintf("%s_modelfile.txt", modelName)) + err = os.WriteFile(newModelfilePath, []byte(modelfileContent), 0644) + if err != nil { + return "", fmt.Errorf("error writing modelfile to temp file: %v", err) + } + defer os.Remove(newModelfilePath) + + // Open the local modelfile in the editor + cmd := exec.Command(os.Getenv("EDITOR"), newModelfilePath) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Run() + if err != nil { + return "", fmt.Errorf("error running editor: %v", err) + } + + // Read the edited content from the local file + newModelfileContent, err := os.ReadFile(newModelfilePath) + if err != nil { + return "", fmt.Errorf("error reading edited modelfile: %v", err) + } + + // If there were no changes, return early + if string(newModelfileContent) == modelfileContent { + return fmt.Sprintf("No changes made to model %s", modelName), nil + } + + // Update the model on the server with the new modelfile content + createReq := &api.CreateRequest{ + Name: modelName, + Modelfile: string(newModelfileContent), + } + + err = client.Create(ctx, createReq, func(resp api.ProgressResponse) error { + logging.InfoLogger.Printf("Create progress: %s\n", resp.Status) + return nil + }) + if err != nil { + return "", fmt.Errorf("error updating model with new modelfile: %v", err) + } + + // log to the console if we're not in a tea app + fmt.Printf("Model %s updated successfully\n", modelName) + + return fmt.Sprintf("Model %s updated successfully, Press 'q' to return to the models list", modelName), nil +}