From 66d231e1944521cf1d2cf31018932d9929c9cb5a Mon Sep 17 00:00:00 2001 From: macie Date: Sat, 30 Dec 2023 22:58:31 +0100 Subject: [PATCH] feat: First working version of CLI Currently supports prompt completion only. --- cmd/boludo/config.go | 70 +++++++++++++++++++++++++++++++++++++ cmd/boludo/main.go | 83 ++++++++++++++++++++++++++++---------------- llama/llama.go | 26 +++++++++++--- 3 files changed, 144 insertions(+), 35 deletions(-) diff --git a/cmd/boludo/config.go b/cmd/boludo/config.go index 6c0a789..63c5de7 100644 --- a/cmd/boludo/config.go +++ b/cmd/boludo/config.go @@ -6,12 +6,14 @@ package main import ( "context" + "errors" "flag" "fmt" "io" "io/fs" "os" "os/signal" + "path/filepath" "strings" "time" @@ -35,6 +37,74 @@ const helpMsg = "boludo - AI personal assistant\n" + "\n" + "boludo reads prompt from PROMPT, and then from standard input" +var AppVersion = "local-dev" + +// Version returns string with full version description. +func Version() string { + return fmt.Sprintf("boludo %s", AppVersion) +} + +// AppConfig contains configuration options for the program. +type AppConfig struct { + Options llama.Options + ServerPath string + Prompt string + Timeout time.Duration + Verbose bool + ExitMessage string +} + +// NewAppConfig creates a new AppConfig from: +// - command line arguments +// - config file +// - default values +// +// in that order. +func NewAppConfig(cliArgs []string) (AppConfig, error) { + configArgs, err := ParseArgs(cliArgs) + if err != nil { + return AppConfig{}, fmt.Errorf("could not read CLI arguments: %w", err) + } + if configArgs.ShowHelp { + return AppConfig{ExitMessage: helpMsg}, nil + } + if configArgs.ShowVersion { + return AppConfig{ExitMessage: Version()}, nil + } + + configRoot, err := os.UserConfigDir() + if err != nil { + return AppConfig{}, fmt.Errorf("could not locate config directory: %w", err) + } + configDir := filepath.Join(configRoot, "boludo") + configFile, err := ParseFile(os.DirFS(configDir), "boludo.toml") + switch { + case err == nil: + // no error + case errors.Is(err, os.ErrNotExist): + // ignore missing config file + default: + return AppConfig{}, fmt.Errorf("could not read `%s`: %w", filepath.Join(configDir, "boludo.toml"), err) + } + + options := llama.DefaultOptions + options.Update(configFile.Options(configArgs.ConfigId)) + options.Update(configArgs.Options()) + + prompt := configArgs.Prompt + if spec, ok := configFile[configArgs.ConfigId]; ok { + prompt = fmt.Sprintf("%s %s", spec.InitialPrompt, configArgs.Prompt) + } + + return AppConfig{ + Prompt: prompt, + Options: options, + ServerPath: configArgs.ServerPath, + Timeout: configArgs.Timeout, + Verbose: configArgs.ShowVerbose, + }, nil +} + // NewAppContext returns a cancellable context which is: // - cancelled when the interrupt signal is received // - cancelled after the timeout (if any). diff --git a/cmd/boludo/main.go b/cmd/boludo/main.go index e9db05b..266ea85 100644 --- a/cmd/boludo/main.go +++ b/cmd/boludo/main.go @@ -5,48 +5,71 @@ package main import ( - "bufio" + "context" "fmt" - "io" + "log/slog" "os" - "strings" + + "github.com/macie/boludo" + "github.com/macie/boludo/llama" ) func main() { - fmt.Fprintln(os.Stdout, "Hello! I'm boludo. You can ask me anything or enter empty line to exit.") - fmt.Fprintln(os.Stdout, "") - fmt.Fprintln(os.Stdout, "How can I help you?") + defaultLogHandler := boludo.UnstructuredHandler{Prefix: "[boludo]", Level: slog.LevelError} + slog.SetDefault(slog.New(defaultLogHandler)) - for { - fmt.Fprint(os.Stdout, "> ") - output, err := readline(os.Stdin) - if err != nil { - fmt.Fprintf(os.Stderr, "ERROR: %s\n", err) - os.Exit(1) - } + config, err := NewAppConfig(os.Args[1:]) + if err != nil { + slog.Error(fmt.Sprint(err)) + os.Exit(1) + } - if fmt.Sprint(output) == "" { - fmt.Fprintln(os.Stdout, "< Goodbye!") - break - } + if config.ExitMessage != "" { + fmt.Fprintln(os.Stdin, config.ExitMessage) + os.Exit(0) + } + if config.Verbose { + defaultLogHandler.Level = slog.LevelInfo + slog.SetDefault(slog.New(defaultLogHandler)) + } - fmt.Fprintf(os.Stdout, "< I don't understand: %s", output) - fmt.Fprintln(os.Stdout, "") - fmt.Fprintln(os.Stdout, "") + ctx, cancel := NewAppContext(config) + defer cancel() + + server := llama.Server{ + Path: config.ServerPath, + Logger: slog.New(boludo.UnstructuredHandler{Prefix: "[llm-server]", Level: defaultLogHandler.Level}), } + llama.SetDefault(server) - os.Exit(0) -} + if err := llama.Serve(ctx, config.Options.ModelPath); err != nil { + slog.Error(fmt.Sprint(err)) + os.Exit(1) + } + defer llama.Close() + + output, err := llama.Complete(ctx, config.Prompt) + if err != nil { + slog.Error(fmt.Sprint(err)) + os.Exit(1) + } -func readline(r io.Reader) (io.Writer, error) { - s := bufio.NewScanner(r) - output := new(strings.Builder) + fmt.Fprint(os.Stdout, config.Prompt) - s.Scan() - output.WriteString(strings.TrimRight(s.Text(), " ")) - if err := s.Err(); err != nil { - return nil, err + for token := range output { + fmt.Fprint(os.Stdout, token) } - return output, nil + switch ctx.Err() { + case nil: + // no error + case context.Canceled: + slog.Info("completion cancelled by user") + case context.DeadlineExceeded: + slog.Info("completion needs more time than expected") + default: + slog.Info("completion was interrupted") + } + + os.Exit(0) } diff --git a/llama/llama.go b/llama/llama.go index b45a12d..8ada8f7 100644 --- a/llama/llama.go +++ b/llama/llama.go @@ -11,12 +11,12 @@ import ( var ( defaultServer = Server{Addr: "localhost:24114"} defaultClient = Client{Addr: "localhost:24114"} -// DefaultOptions represent neutral parameters for interacting with LLaMA model. + // DefaultOptions represent neutral parameters for interacting with LLaMA model. DefaultOptions = Options{ - ModelPath: "", - Seed: 0, - Temp: 1, - MinP: 0, + ModelPath: "", + Seed: 0, + Temp: 1, + MinP: 0, } ) @@ -50,3 +50,19 @@ type Options struct { MinP float32 Seed uint } + +// Update updates the Options with the non-default values from other Options. +func (o *Options) Update(other Options) { + if other.ModelPath != DefaultOptions.ModelPath { + o.ModelPath = other.ModelPath + } + if other.Temp != DefaultOptions.Temp { + o.Temp = other.Temp + } + if other.MinP != DefaultOptions.MinP { + o.MinP = other.MinP + } + if other.Seed != DefaultOptions.Seed { + o.Seed = other.Seed + } +}