Skip to content

Commit

Permalink
feat: First working version of CLI
Browse files Browse the repository at this point in the history
Currently supports prompt completion only.
  • Loading branch information
macie committed Dec 30, 2023
1 parent 0379822 commit 66d231e
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 35 deletions.
70 changes: 70 additions & 0 deletions cmd/boludo/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ package main

import (
"context"
"errors"
"flag"
"fmt"
"io"
"io/fs"
"os"
"os/signal"
"path/filepath"
"strings"
"time"

Expand All @@ -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).
Expand Down
83 changes: 53 additions & 30 deletions cmd/boludo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
26 changes: 21 additions & 5 deletions llama/llama.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
)

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

0 comments on commit 66d231e

Please sign in to comment.