diff --git a/cmd/interactive.go b/cmd/interactive.go index c3cdf629..70afc6ea 100644 --- a/cmd/interactive.go +++ b/cmd/interactive.go @@ -1,6 +1,7 @@ package cmd import ( + "cmp" "errors" "fmt" "io" @@ -9,13 +10,14 @@ import ( "path/filepath" "regexp" "slices" - "sort" "strings" "github.com/spf13/cobra" + "golang.org/x/exp/maps" "github.com/ollama/ollama/api" "github.com/ollama/ollama/envconfig" + "github.com/ollama/ollama/parser" "github.com/ollama/ollama/progress" "github.com/ollama/ollama/readline" "github.com/ollama/ollama/types/errtypes" @@ -376,9 +378,9 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error { return err } req := &api.ShowRequest{ - Name: opts.Model, - System: opts.System, - Options: opts.Options, + Name: opts.Model, + System: opts.System, + Options: opts.Options, } resp, err := client.Show(cmd.Context(), req) if err != nil { @@ -507,31 +509,35 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error { } func buildModelfile(opts runOptions) string { - var mf strings.Builder - model := opts.ParentModel - if model == "" { - model = opts.Model - } - fmt.Fprintf(&mf, "FROM %s\n", model) + var f parser.File + f.Commands = append(f.Commands, parser.Command{Name: "model", Args: cmp.Or(opts.ParentModel, opts.Model)}) + if opts.System != "" { - fmt.Fprintf(&mf, "SYSTEM \"\"\"%s\"\"\"\n", opts.System) + f.Commands = append(f.Commands, parser.Command{Name: "system", Args: opts.System}) } - keys := make([]string, 0) - for k := range opts.Options { - keys = append(keys, k) - } - sort.Strings(keys) + keys := maps.Keys(opts.Options) + slices.Sort(keys) for _, k := range keys { - fmt.Fprintf(&mf, "PARAMETER %s %v\n", k, opts.Options[k]) + v := opts.Options[k] + var cmds []parser.Command + switch t := v.(type) { + case []string: + for _, s := range t { + cmds = append(cmds, parser.Command{Name: k, Args: s}) + } + default: + cmds = append(cmds, parser.Command{Name: k, Args: fmt.Sprintf("%v", t)}) + } + + f.Commands = append(f.Commands, cmds...) } - fmt.Fprintln(&mf) for _, msg := range opts.Messages { - fmt.Fprintf(&mf, "MESSAGE %s \"\"\"%s\"\"\"\n", msg.Role, msg.Content) + f.Commands = append(f.Commands, parser.Command{Name: "message", Args: fmt.Sprintf("%s: %s", msg.Role, msg.Content)}) } - return mf.String() + return f.String() } func normalizeFilePath(fp string) string { diff --git a/cmd/interactive_test.go b/cmd/interactive_test.go index 711f3860..bb7e0aba 100644 --- a/cmd/interactive_test.go +++ b/cmd/interactive_test.go @@ -1,12 +1,10 @@ package cmd import ( - "bytes" "testing" - "text/template" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "github.com/ollama/ollama/api" ) @@ -57,58 +55,53 @@ d:\path with\spaces\seven.svg inbetween7 c:\users\jdoe\eight.png inbetween8 func TestModelfileBuilder(t *testing.T) { opts := runOptions{ - Model: "hork", - System: "You are part horse and part shark, but all hork. Do horklike things", + Model: "hork", + System: "You are part horse and part shark, but all hork. Do horklike things", Messages: []api.Message{ {Role: "user", Content: "Hey there hork!"}, {Role: "assistant", Content: "Yes it is true, I am half horse, half shark."}, }, - Options: map[string]interface{}{}, + Options: map[string]any{ + "temperature": 0.9, + "seed": 42, + "penalize_newline": false, + "stop": []string{"hi", "there"}, + }, } - opts.Options["temperature"] = 0.9 - opts.Options["seed"] = 42 - opts.Options["penalize_newline"] = false - opts.Options["stop"] = []string{"hi", "there"} - - mf := buildModelfile(opts) - expectedModelfile := `FROM {{.Model}} -SYSTEM """{{.System}}""" + t.Run("model", func(t *testing.T) { + expect := `FROM hork +SYSTEM You are part horse and part shark, but all hork. Do horklike things PARAMETER penalize_newline false PARAMETER seed 42 -PARAMETER stop [hi there] +PARAMETER stop hi +PARAMETER stop there PARAMETER temperature 0.9 - -MESSAGE user """Hey there hork!""" -MESSAGE assistant """Yes it is true, I am half horse, half shark.""" +MESSAGE user Hey there hork! +MESSAGE assistant Yes it is true, I am half horse, half shark. ` - tmpl, err := template.New("").Parse(expectedModelfile) - require.NoError(t, err) + actual := buildModelfile(opts) + if diff := cmp.Diff(expect, actual); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + }) - var buf bytes.Buffer - err = tmpl.Execute(&buf, opts) - require.NoError(t, err) - assert.Equal(t, buf.String(), mf) - - opts.ParentModel = "horseshark" - mf = buildModelfile(opts) - expectedModelfile = `FROM {{.ParentModel}} -SYSTEM """{{.System}}""" + t.Run("parent model", func(t *testing.T) { + opts.ParentModel = "horseshark" + expect := `FROM horseshark +SYSTEM You are part horse and part shark, but all hork. Do horklike things PARAMETER penalize_newline false PARAMETER seed 42 -PARAMETER stop [hi there] +PARAMETER stop hi +PARAMETER stop there PARAMETER temperature 0.9 - -MESSAGE user """Hey there hork!""" -MESSAGE assistant """Yes it is true, I am half horse, half shark.""" +MESSAGE user Hey there hork! +MESSAGE assistant Yes it is true, I am half horse, half shark. ` - - tmpl, err = template.New("").Parse(expectedModelfile) - require.NoError(t, err) - - var parentBuf bytes.Buffer - err = tmpl.Execute(&parentBuf, opts) - require.NoError(t, err) - assert.Equal(t, parentBuf.String(), mf) + actual := buildModelfile(opts) + if diff := cmp.Diff(expect, actual); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + }) }