interactive.go 18.5 KB
Newer Older
1
2
3
4
5
6
7
8
package cmd

import (
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
9
	"path/filepath"
10
	"regexp"
11
	"slices"
12
	"sort"
13
14
15
16
	"strings"

	"github.com/spf13/cobra"

17
	"github.com/ollama/ollama/api"
18
	"github.com/ollama/ollama/envconfig"
19
20
	"github.com/ollama/ollama/progress"
	"github.com/ollama/ollama/readline"
21
	"github.com/ollama/ollama/types/errtypes"
22
23
24
25
26
27
28
29
30
31
32
)

type MultilineState int

const (
	MultilineNone MultilineState = iota
	MultilinePrompt
	MultilineSystem
	MultilineTemplate
)

33
34
35
36
37
38
39
func loadModel(cmd *cobra.Command, opts *runOptions) error {
	p := progress.NewProgress(os.Stderr)
	defer p.StopAndClear()

	spinner := progress.NewSpinner("")
	p.Add("", spinner)

40
	client, err := api.ClientFromEnvironment()
41
	if err != nil {
42
		return err
43
44
	}

45
	chatReq := &api.ChatRequest{
46
47
		Model:     opts.Model,
		KeepAlive: opts.KeepAlive,
48
	}
49

50
	return client.Chat(cmd.Context(), chatReq, func(resp api.ChatResponse) error {
51
		p.StopAndClear()
52
53
54
55
56
57
58
59
60
		for _, msg := range opts.Messages {
			switch msg.Role {
			case "user":
				fmt.Printf(">>> %s\n", msg.Content)
			case "assistant":
				state := &displayResponseState{}
				displayResponse(msg.Content, opts.WordWrap, state)
				fmt.Println()
				fmt.Println()
61
62
63
64
65
66
67
68
69
			}
		}
		return nil
	})
}

func generateInteractive(cmd *cobra.Command, opts runOptions) error {
	err := loadModel(cmd, &opts)
	if err != nil {
70
71
72
73
74
		return err
	}

	usage := func() {
		fmt.Fprintln(os.Stderr, "Available Commands:")
75
76
77
78
		fmt.Fprintln(os.Stderr, "  /set            Set session variables")
		fmt.Fprintln(os.Stderr, "  /show           Show model information")
		fmt.Fprintln(os.Stderr, "  /load <model>   Load a session or model")
		fmt.Fprintln(os.Stderr, "  /save <model>   Save your current session")
Bryce Reitano's avatar
Bryce Reitano committed
79
		fmt.Fprintln(os.Stderr, "  /clear          Clear session context")
80
81
82
		fmt.Fprintln(os.Stderr, "  /bye            Exit")
		fmt.Fprintln(os.Stderr, "  /?, /help       Help for a command")
		fmt.Fprintln(os.Stderr, "  /? shortcuts    Help for keyboard shortcuts")
83
84
		fmt.Fprintln(os.Stderr, "")
		fmt.Fprintln(os.Stderr, "Use \"\"\" to begin a multi-line message.")
85
86
87
88
89

		if opts.MultiModal {
			fmt.Fprintf(os.Stderr, "Use %s to include .jpg or .png images.\n", filepath.FromSlash("/path/to/file"))
		}

90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
		fmt.Fprintln(os.Stderr, "")
	}

	usageSet := func() {
		fmt.Fprintln(os.Stderr, "Available Commands:")
		fmt.Fprintln(os.Stderr, "  /set parameter ...     Set a parameter")
		fmt.Fprintln(os.Stderr, "  /set system <string>   Set system message")
		fmt.Fprintln(os.Stderr, "  /set template <string> Set prompt template")
		fmt.Fprintln(os.Stderr, "  /set history           Enable history")
		fmt.Fprintln(os.Stderr, "  /set nohistory         Disable history")
		fmt.Fprintln(os.Stderr, "  /set wordwrap          Enable wordwrap")
		fmt.Fprintln(os.Stderr, "  /set nowordwrap        Disable wordwrap")
		fmt.Fprintln(os.Stderr, "  /set format json       Enable JSON mode")
		fmt.Fprintln(os.Stderr, "  /set noformat          Disable formatting")
		fmt.Fprintln(os.Stderr, "  /set verbose           Show LLM stats")
		fmt.Fprintln(os.Stderr, "  /set quiet             Disable LLM stats")
		fmt.Fprintln(os.Stderr, "")
	}

	usageShortcuts := func() {
		fmt.Fprintln(os.Stderr, "Available keyboard shortcuts:")
		fmt.Fprintln(os.Stderr, "  Ctrl + a            Move to the beginning of the line (Home)")
		fmt.Fprintln(os.Stderr, "  Ctrl + e            Move to the end of the line (End)")
		fmt.Fprintln(os.Stderr, "   Alt + b            Move back (left) one word")
		fmt.Fprintln(os.Stderr, "   Alt + f            Move forward (right) one word")
		fmt.Fprintln(os.Stderr, "  Ctrl + k            Delete the sentence after the cursor")
		fmt.Fprintln(os.Stderr, "  Ctrl + u            Delete the sentence before the cursor")
Josh Yan's avatar
Josh Yan committed
117
		fmt.Fprintln(os.Stderr, "  Ctrl + w            Delete the word before the cursor")
118
119
120
121
122
123
124
125
126
		fmt.Fprintln(os.Stderr, "")
		fmt.Fprintln(os.Stderr, "  Ctrl + l            Clear the screen")
		fmt.Fprintln(os.Stderr, "  Ctrl + c            Stop the model from responding")
		fmt.Fprintln(os.Stderr, "  Ctrl + d            Exit ollama (/bye)")
		fmt.Fprintln(os.Stderr, "")
	}

	usageShow := func() {
		fmt.Fprintln(os.Stderr, "Available Commands:")
127
		fmt.Fprintln(os.Stderr, "  /show info         Show details for this model")
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
		fmt.Fprintln(os.Stderr, "  /show license      Show model license")
		fmt.Fprintln(os.Stderr, "  /show modelfile    Show Modelfile for this model")
		fmt.Fprintln(os.Stderr, "  /show parameters   Show parameters for this model")
		fmt.Fprintln(os.Stderr, "  /show system       Show system message")
		fmt.Fprintln(os.Stderr, "  /show template     Show prompt template")
		fmt.Fprintln(os.Stderr, "")
	}

	// only list out the most common parameters
	usageParameters := func() {
		fmt.Fprintln(os.Stderr, "Available Parameters:")
		fmt.Fprintln(os.Stderr, "  /set parameter seed <int>             Random number seed")
		fmt.Fprintln(os.Stderr, "  /set parameter num_predict <int>      Max number of tokens to predict")
		fmt.Fprintln(os.Stderr, "  /set parameter top_k <int>            Pick from top k num of tokens")
		fmt.Fprintln(os.Stderr, "  /set parameter top_p <float>          Pick token based on sum of probabilities")
		fmt.Fprintln(os.Stderr, "  /set parameter num_ctx <int>          Set the context size")
		fmt.Fprintln(os.Stderr, "  /set parameter temperature <float>    Set creativity level")
		fmt.Fprintln(os.Stderr, "  /set parameter repeat_penalty <float> How strongly to penalize repetitions")
		fmt.Fprintln(os.Stderr, "  /set parameter repeat_last_n <int>    Set how far back to look for repetitions")
		fmt.Fprintln(os.Stderr, "  /set parameter num_gpu <int>          The number of layers to send to the GPU")
148
		fmt.Fprintln(os.Stderr, "  /set parameter stop <string> <string> ...   Set the stop parameters")
149
150
151
152
153
154
155
156
157
158
159
160
161
		fmt.Fprintln(os.Stderr, "")
	}

	scanner, err := readline.New(readline.Prompt{
		Prompt:         ">>> ",
		AltPrompt:      "... ",
		Placeholder:    "Send a message (/? for help)",
		AltPlaceholder: `Use """ to end multi-line input`,
	})
	if err != nil {
		return err
	}

162
	if envconfig.NoHistory {
163
164
165
		scanner.HistoryDisable()
	}

166
167
168
	fmt.Print(readline.StartBracketedPaste)
	defer fmt.Printf(readline.EndBracketedPaste)

169
	var sb strings.Builder
170
171
172
173
174
175
176
177
178
179
180
181
182
183
	var multiline MultilineState

	for {
		line, err := scanner.Readline()
		switch {
		case errors.Is(err, io.EOF):
			fmt.Println()
			return nil
		case errors.Is(err, readline.ErrInterrupt):
			if line == "" {
				fmt.Println("\nUse Ctrl + d or /bye to exit.")
			}

			scanner.Prompt.UseAlt = false
184
			sb.Reset()
185
186
187
188
189
190
191

			continue
		case err != nil:
			return err
		}

		switch {
192
193
194
195
196
197
		case multiline != MultilineNone:
			// check if there's a multiline terminating string
			before, ok := strings.CutSuffix(line, `"""`)
			sb.WriteString(before)
			if !ok {
				fmt.Fprintln(&sb)
198
199
200
201
202
				continue
			}

			switch multiline {
			case MultilineSystem:
203
				opts.System = sb.String()
204
				opts.Messages = append(opts.Messages, api.Message{Role: "system", Content: opts.System})
205
				fmt.Println("Set system message.")
206
				sb.Reset()
207
			case MultilineTemplate:
208
				opts.Template = sb.String()
209
				fmt.Println("Set prompt template.")
210
				sb.Reset()
211
			}
212

213
			multiline = MultilineNone
214
215
216
217
218
219
220
221
222
223
224
			scanner.Prompt.UseAlt = false
		case strings.HasPrefix(line, `"""`):
			line := strings.TrimPrefix(line, `"""`)
			line, ok := strings.CutSuffix(line, `"""`)
			sb.WriteString(line)
			if !ok {
				// no multiline terminating string; need more input
				fmt.Fprintln(&sb)
				multiline = MultilinePrompt
				scanner.Prompt.UseAlt = true
			}
225
		case scanner.Pasting:
226
			fmt.Fprintln(&sb, line)
227
228
229
230
231
232
			continue
		case strings.HasPrefix(line, "/list"):
			args := strings.Fields(line)
			if err := ListHandler(cmd, args[1:]); err != nil {
				return err
			}
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
		case strings.HasPrefix(line, "/load"):
			args := strings.Fields(line)
			if len(args) != 2 {
				fmt.Println("Usage:\n  /load <modelname>")
				continue
			}
			opts.Model = args[1]
			opts.Messages = []api.Message{}
			fmt.Printf("Loading model '%s'\n", opts.Model)
			if err := loadModel(cmd, &opts); err != nil {
				return err
			}
			continue
		case strings.HasPrefix(line, "/save"):
			args := strings.Fields(line)
			if len(args) != 2 {
				fmt.Println("Usage:\n  /save <modelname>")
				continue
			}

			client, err := api.ClientFromEnvironment()
			if err != nil {
				fmt.Println("error: couldn't connect to ollama server")
				return err
			}

			req := &api.CreateRequest{
				Name:      args[1],
				Modelfile: buildModelfile(opts),
			}
			fn := func(resp api.ProgressResponse) error { return nil }
			err = client.Create(cmd.Context(), req, fn)
			if err != nil {
266
267
268
269
				if strings.Contains(err.Error(), errtypes.InvalidModelNameErrMsg) {
					fmt.Printf("error: The model name '%s' is invalid\n", args[1])
					continue
				}
270
271
272
273
				return err
			}
			fmt.Printf("Created new model '%s'\n", args[1])
			continue
Bryce Reitano's avatar
Bryce Reitano committed
274
275
		case strings.HasPrefix(line, "/clear"):
			opts.Messages = []api.Message{}
Patrick Devine's avatar
Patrick Devine committed
276
277
278
279
			if opts.System != "" {
				newMessage := api.Message{Role: "system", Content: opts.System}
				opts.Messages = append(opts.Messages, newMessage)
			}
Bryce Reitano's avatar
Bryce Reitano committed
280
281
			fmt.Println("Cleared session context")
			continue
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
		case strings.HasPrefix(line, "/set"):
			args := strings.Fields(line)
			if len(args) > 1 {
				switch args[1] {
				case "history":
					scanner.HistoryEnable()
				case "nohistory":
					scanner.HistoryDisable()
				case "wordwrap":
					opts.WordWrap = true
					fmt.Println("Set 'wordwrap' mode.")
				case "nowordwrap":
					opts.WordWrap = false
					fmt.Println("Set 'nowordwrap' mode.")
				case "verbose":
297
298
299
					if err := cmd.Flags().Set("verbose", "true"); err != nil {
						return err
					}
300
301
					fmt.Println("Set 'verbose' mode.")
				case "quiet":
302
303
304
					if err := cmd.Flags().Set("verbose", "false"); err != nil {
						return err
					}
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
					fmt.Println("Set 'quiet' mode.")
				case "format":
					if len(args) < 3 || args[2] != "json" {
						fmt.Println("Invalid or missing format. For 'json' mode use '/set format json'")
					} else {
						opts.Format = args[2]
						fmt.Printf("Set format to '%s' mode.\n", args[2])
					}
				case "noformat":
					opts.Format = ""
					fmt.Println("Disabled format.")
				case "parameter":
					if len(args) < 4 {
						usageParameters()
						continue
					}
Michael Yang's avatar
Michael Yang committed
321
					params := args[3:]
322
323
					fp, err := api.FormatParams(map[string][]string{args[2]: params})
					if err != nil {
324
						fmt.Printf("Couldn't set parameter: %q\n", err)
325
326
						continue
					}
327
					fmt.Printf("Set parameter '%s' to '%s'\n", args[2], strings.Join(params, ", "))
328
329
330
331
332
333
					opts.Options[args[2]] = fp[args[2]]
				case "system", "template":
					if len(args) < 3 {
						usageSet()
						continue
					}
334
335
336
337
338
339
340

					if args[1] == "system" {
						multiline = MultilineSystem
					} else if args[1] == "template" {
						multiline = MultilineTemplate
					}

341
					line := strings.Join(args[2:], " ")
342
343
344
					line, ok := strings.CutPrefix(line, `"""`)
					if !ok {
						multiline = MultilineNone
345
					} else {
346
347
348
349
350
351
352
353
354
355
356
357
358
359
						// only cut suffix if the line is multiline
						line, ok = strings.CutSuffix(line, `"""`)
						if ok {
							multiline = MultilineNone
						}
					}

					sb.WriteString(line)
					if multiline != MultilineNone {
						scanner.Prompt.UseAlt = true
						continue
					}

					if args[1] == "system" {
360
361
362
363
364
365
366
367
368
						opts.System = sb.String() // for display in modelfile
						newMessage := api.Message{Role: "system", Content: sb.String()}
						// Check if the slice is not empty and the last message is from 'system'
						if len(opts.Messages) > 0 && opts.Messages[len(opts.Messages)-1].Role == "system" {
							// Replace the last message
							opts.Messages[len(opts.Messages)-1] = newMessage
						} else {
							opts.Messages = append(opts.Messages, newMessage)
						}
369
						fmt.Println("Set system message.")
370
						sb.Reset()
371
372
373
					} else if args[1] == "template" {
						opts.Template = sb.String()
						fmt.Println("Set prompt template.")
374
						sb.Reset()
375
					}
376
377
378

					sb.Reset()
					continue
379
380
381
382
383
384
385
386
387
388
389
390
391
392
				default:
					fmt.Printf("Unknown command '/set %s'. Type /? for help\n", args[1])
				}
			} else {
				usageSet()
			}
		case strings.HasPrefix(line, "/show"):
			args := strings.Fields(line)
			if len(args) > 1 {
				client, err := api.ClientFromEnvironment()
				if err != nil {
					fmt.Println("error: couldn't connect to ollama server")
					return err
				}
393
				req := &api.ShowRequest{
394
					Name:     opts.Model,
395
396
397
398
399
					System:   opts.System,
					Template: opts.Template,
					Options:  opts.Options,
				}
				resp, err := client.Show(cmd.Context(), req)
400
401
402
403
404
405
				if err != nil {
					fmt.Println("error: couldn't get model")
					return err
				}

				switch args[1] {
406
				case "info":
407
					showInfo(resp)
408
409
				case "license":
					if resp.License == "" {
410
						fmt.Println("No license was specified for this model.")
411
412
413
414
415
416
417
					} else {
						fmt.Println(resp.License)
					}
				case "modelfile":
					fmt.Println(resp.Modelfile)
				case "parameters":
					if resp.Parameters == "" {
418
						fmt.Println("No parameters were specified for this model.")
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
					} else {
						if len(opts.Options) > 0 {
							fmt.Println("User defined parameters:")
							for k, v := range opts.Options {
								fmt.Printf("%-*s %v\n", 30, k, v)
							}
							fmt.Println()
						}
						fmt.Println("Model defined parameters:")
						fmt.Println(resp.Parameters)
					}
				case "system":
					switch {
					case opts.System != "":
						fmt.Println(opts.System + "\n")
					case resp.System != "":
						fmt.Println(resp.System + "\n")
					default:
437
						fmt.Println("No system message was specified for this model.")
438
439
440
441
442
443
444
445
					}
				case "template":
					switch {
					case opts.Template != "":
						fmt.Println(opts.Template + "\n")
					case resp.Template != "":
						fmt.Println(resp.Template)
					default:
446
						fmt.Println("No prompt template was specified for this model.")
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
					}
				default:
					fmt.Printf("Unknown command '/show %s'. Type /? for help\n", args[1])
				}
			} else {
				usageShow()
			}
		case strings.HasPrefix(line, "/help"), strings.HasPrefix(line, "/?"):
			args := strings.Fields(line)
			if len(args) > 1 {
				switch args[1] {
				case "set", "/set":
					usageSet()
				case "show", "/show":
					usageShow()
				case "shortcut", "shortcuts":
					usageShortcuts()
				}
			} else {
				usage()
			}
468
		case strings.HasPrefix(line, "/exit"), strings.HasPrefix(line, "/bye"):
469
470
471
472
473
			return nil
		case strings.HasPrefix(line, "/"):
			args := strings.Fields(line)
			isFile := false

474
			if opts.MultiModal {
475
476
477
478
479
480
481
482
				for _, f := range extractFileNames(line) {
					if strings.HasPrefix(f, args[0]) {
						isFile = true
						break
					}
				}
			}

483
			if !isFile {
484
485
486
				fmt.Printf("Unknown command '%s'. Type /? for help\n", args[0])
				continue
			}
487
488

			sb.WriteString(line)
489
		default:
490
			sb.WriteString(line)
491
492
		}

493
		if sb.Len() > 0 && multiline == MultilineNone {
494
495
			newMessage := api.Message{Role: "user", Content: sb.String()}

496
			if opts.MultiModal {
497
				msg, images, err := extractFileData(sb.String())
498
499
500
				if err != nil {
					return err
				}
501
502
503
504
505
506
507
508

				// clear all previous images for better responses
				if len(images) > 0 {
					for i := range opts.Messages {
						opts.Messages[i].Images = nil
					}
				}

509
				newMessage.Content = msg
510
				newMessage.Images = images
511
			}
512

513
514
515
516
			opts.Messages = append(opts.Messages, newMessage)

			assistant, err := chat(cmd, opts)
			if err != nil {
517
518
				return err
			}
519
520
521
			if assistant != nil {
				opts.Messages = append(opts.Messages, *assistant)
			}
522

523
			sb.Reset()
524
525
526
527
		}
	}
}

528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
func buildModelfile(opts runOptions) string {
	var mf strings.Builder
	model := opts.ParentModel
	if model == "" {
		model = opts.Model
	}
	fmt.Fprintf(&mf, "FROM %s\n", model)
	if opts.System != "" {
		fmt.Fprintf(&mf, "SYSTEM \"\"\"%s\"\"\"\n", opts.System)
	}

	if opts.Template != "" {
		fmt.Fprintf(&mf, "TEMPLATE \"\"\"%s\"\"\"\n", opts.Template)
	}

	keys := make([]string, 0)
	for k := range opts.Options {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	for _, k := range keys {
		fmt.Fprintf(&mf, "PARAMETER %s %v\n", k, opts.Options[k])
	}
	fmt.Fprintln(&mf)

	for _, msg := range opts.Messages {
		fmt.Fprintf(&mf, "MESSAGE %s \"\"\"%s\"\"\"\n", msg.Role, msg.Content)
	}

	return mf.String()
}

560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
func normalizeFilePath(fp string) string {
	// Define a map of escaped characters and their replacements
	replacements := map[string]string{
		"\\ ":  " ",  // Escaped space
		"\\(":  "(",  // Escaped left parenthesis
		"\\)":  ")",  // Escaped right parenthesis
		"\\[":  "[",  // Escaped left square bracket
		"\\]":  "]",  // Escaped right square bracket
		"\\{":  "{",  // Escaped left curly brace
		"\\}":  "}",  // Escaped right curly brace
		"\\$":  "$",  // Escaped dollar sign
		"\\&":  "&",  // Escaped ampersand
		"\\;":  ";",  // Escaped semicolon
		"\\'":  "'",  // Escaped single quote
		"\\\\": "\\", // Escaped backslash
		"\\*":  "*",  // Escaped asterisk
		"\\?":  "?",  // Escaped question mark
	}

	for escaped, actual := range replacements {
		fp = strings.ReplaceAll(fp, escaped, actual)
	}
	return fp
}

func extractFileNames(input string) []string {
586
	// Regex to match file paths starting with optional drive letter, / ./ \ or .\ and include escaped or unescaped spaces (\ or %20)
587
	// and followed by more characters and a file extension
588
589
	// This will capture non filename strings, but we'll check for file existence to remove mismatches
	regexPattern := `(?:[a-zA-Z]:)?(?:\./|/|\\)[\S\\ ]+?\.(?i:jpg|jpeg|png|svg)\b`
590
591
592
593
594
	re := regexp.MustCompile(regexPattern)

	return re.FindAllString(input, -1)
}

595
func extractFileData(input string) (string, []api.ImageData, error) {
596
	filePaths := extractFileNames(input)
597
	var imgs []api.ImageData
598
599
600
601
602
603
604
605

	for _, fp := range filePaths {
		nfp := normalizeFilePath(fp)
		data, err := getImageData(nfp)
		if err != nil {
			if os.IsNotExist(err) {
				continue
			}
606
			fmt.Fprintf(os.Stderr, "Couldn't process image: %q\n", err)
607
608
			return "", imgs, err
		}
609
		fmt.Fprintf(os.Stderr, "Added image '%s'\n", nfp)
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
		input = strings.ReplaceAll(input, fp, "")
		imgs = append(imgs, data)
	}
	return input, imgs, nil
}

func getImageData(filePath string) ([]byte, error) {
	file, err := os.Open(filePath)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	buf := make([]byte, 512)
	_, err = file.Read(buf)
	if err != nil {
		return nil, err
	}

	contentType := http.DetectContentType(buf)
630
	allowedTypes := []string{"image/jpeg", "image/jpg", "image/png"}
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
	if !slices.Contains(allowedTypes, contentType) {
		return nil, fmt.Errorf("invalid image type: %s", contentType)
	}

	info, err := file.Stat()
	if err != nil {
		return nil, err
	}

	// Check if the file size exceeds 100MB
	var maxSize int64 = 100 * 1024 * 1024 // 100MB in bytes
	if info.Size() > maxSize {
		return nil, fmt.Errorf("file size exceeds maximum limit (100MB)")
	}

	buf = make([]byte, info.Size())
	_, err = file.Seek(0, 0)
	if err != nil {
		return nil, err
	}

	_, err = io.ReadFull(file, buf)
	if err != nil {
		return nil, err
	}

	return buf, nil
}