Skip to content

Commit

Permalink
feat: add edit model cli and tui (#64)
Browse files Browse the repository at this point in the history
  • Loading branch information
sammcj authored Jul 4, 2024
1 parent 8d75d56 commit 174673d
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 34 deletions.
52 changes: 38 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 term>`: Search for models by name
- `-s <search term>`: 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 <model>`: 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

Expand All @@ -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

Expand Down Expand Up @@ -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

Expand Down
25 changes: 15 additions & 10 deletions app_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
}
}

Expand Down
4 changes: 2 additions & 2 deletions keymap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")),
Expand All @@ -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")),
}
}
Expand Down
15 changes: 13 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()

Expand Down Expand Up @@ -248,6 +249,16 @@ func main() {
os.Exit(0)
}

if *editFlag {
if flag.NArg() == 0 {
fmt.Println("Usage: gollama -e <model_name>")
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)
Expand Down Expand Up @@ -276,7 +287,7 @@ func main() {
keys.CopyModel,
keys.PushModel,
keys.Top,
keys.UpdateModel,
keys.EditModel,
keys.Help,
}
}
Expand Down
79 changes: 73 additions & 6 deletions operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}
})
Expand All @@ -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
}

Expand All @@ -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}
})
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
}

0 comments on commit 174673d

Please sign in to comment.