From c35967f79752996ffcd36da11e259dfdc08c7f00 Mon Sep 17 00:00:00 2001 From: Matt Low Date: Mon, 30 Oct 2023 21:23:07 +0000 Subject: [PATCH] Initial prototype --- go.mod | 13 ++++++ go.sum | 12 ++++++ main.go | 126 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ openai.go | 36 ++++++++++++++++ util.go | 41 ++++++++++++++++++ 5 files changed, 228 insertions(+) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 openai.go create mode 100644 util.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8a6a052 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module git.mlow.ca/mlow/lmcli + +go 1.19 + +require ( + github.com/sashabaranov/go-openai v1.16.0 + github.com/spf13/cobra v1.7.0 +) + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3d2c5dd --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sashabaranov/go-openai v1.16.0 h1:34W6WV84ey6OpW0p2UewZkdMu82AxGC+BzpU6iiauRw= +github.com/sashabaranov/go-openai v1.16.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..8429223 --- /dev/null +++ b/main.go @@ -0,0 +1,126 @@ +package main + +import ( + "fmt" + "os" + "github.com/spf13/cobra" +) + +type Message struct { + MessageID string + ConversationID string + Conversation Conversation + OriginalContent string + Role string // 'user' or 'assistant' +} + +type Conversation struct { + ID string + Title string +} + +type Context struct { + CurrentConversation string +} + +var rootCmd = &cobra.Command{ + Use: "lm", + Short: "Interact with Large Language Models", + Long: `lm is a CLI tool to interact with OpenAI's GPT 3.5 and GPT 4.`, + Run: func(cmd *cobra.Command, args []string) { + // execute `lm ls` by default + }, +} + +var lsCmd = &cobra.Command{ + Use: "ls", + Short: "List existing conversations", + Long: `List all existing conversations in descending order of recent activity.`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("Listing conversations...") + // Example output, asterisk to indicate current converation + + // $ lm ls + // last hour: + // 98sg - 12 minutes ago - Project discussion + // last day: + // tj3l - 10 hours ago - Deep learning concepts + // last week: + // bwfm - 2 days ago - Machine learning study + // * 8n3h - 3 days ago - Weekend plans + // f3n7 - 6 days ago - CLI development + // last month: + // 5hn2 - 8 days ago - Book club discussion + // b7ze - 20 days ago - Gardening tips and tricks + // last 6 months: + // 3jn2 - 30 days ago - Web development best practices + // 43jk - 2 months ago - Longboard maintenance + // g8d9 - 3 months ago - History book club + // 4lk3 - 4 months ago - Local events and meetups + // 43jn - 6 months ago - Mobile photography techniques + }, +} + +var msgCmd = &cobra.Command{ + Use: "msg", + Short: "Send a message to active conversation", + Long: `Send a message to the active conversation and receive a message from the LLM in return.`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("Sending message to active conversation...") + // If no messsage provided via args, we should open an editor ala `git commit` + // After submitting the message, the + }, +} + +var viewCmd = &cobra.Command{ + Use: "view", + Short: "View messages in a conversation", + Long: `Displays all the messages in a coversation.`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("Displaying conversation messages...") + }, +} + +var newCmd = &cobra.Command{ + Use: "new", + Short: "Start a new conversation", + Long: `Start a new conversation with the Large Language Model.`, + Run: func(cmd *cobra.Command, args []string) { + messageContents, err := InputFromEditor("# What would you like to say?", "message.*.md") + if err != nil { + fmt.Fprintf(os.Stderr, "Error receiving message input: %v\n", err) + os.Exit(1) + } + + if messageContents == "" { + fmt.Fprintf(os.Stderr, "No message was provided.\n") + os.Exit(1) + } + + fmt.Printf("> %s\n", messageContents) + + // Initialize the messages array for this conversation. + messages := []Message{ + { + OriginalContent: messageContents, + Role: "user", + }, + } + + response, err := CreateChatCompletion("You are a helpful assistant.", messages) + if err != nil { + fmt.Fprintf(os.Stderr, "Error getting chat response: %v\n", err) + os.Exit(1) + } + + fmt.Println(response); + }, +} + +func main() { + rootCmd.AddCommand(newCmd) // Add other commands similarly + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/openai.go b/openai.go new file mode 100644 index 0000000..9b76372 --- /dev/null +++ b/openai.go @@ -0,0 +1,36 @@ +package main + +import ( + "context" + "fmt" + "os" + openai "github.com/sashabaranov/go-openai" +) + +// CreateChatCompletion accepts a slice of Message and returns the response +// of the Large Language Model. +func CreateChatCompletion(system string, messages []Message) (string, error) { + client := openai.NewClient(os.Getenv("OPENAI_APIKEY")) + + var openaiMessages []openai.ChatCompletionMessage + for _, m := range(messages) { + openaiMessages = append(openaiMessages, openai.ChatCompletionMessage{ + Role: m.Role, + Content: m.OriginalContent, + }) + } + + resp, err := client.CreateChatCompletion( + context.Background(), + openai.ChatCompletionRequest{ + Model: openai.GPT4, + Messages: openaiMessages, + }, + ) + + if err != nil { + return "", fmt.Errorf("ChatCompletion error: %v\n", err) + } + + return resp.Choices[0].Message.Content, nil +} diff --git a/util.go b/util.go new file mode 100644 index 0000000..27ce1a1 --- /dev/null +++ b/util.go @@ -0,0 +1,41 @@ +package main + +import ( + "os" + "os/exec" +) + +// InputFromEditor retrieves user input by opening an editor on a temporary +// file. Once the editor closes, the contents of the temporary file are +// returned. If the contents exactly match the placeholder (no edits to the +// file were made), then an empty string is returned. +// Example patten: message.*.md +func InputFromEditor(placeholder string, pattern string) (string, error) { + msgFile, _ := os.CreateTemp("/tmp", pattern) + defer os.Remove(msgFile.Name()) + + os.WriteFile(msgFile.Name(), []byte(placeholder), os.ModeAppend) + + editor := os.Getenv("EDITOR") + if editor == "" { + editor = "vim" // default to vim if no EDITOR env variable + } + + execCmd := exec.Command(editor, msgFile.Name()) + execCmd.Stdin = os.Stdin + execCmd.Stdout = os.Stdout + execCmd.Stderr = os.Stderr + + if err := execCmd.Run(); err != nil { + return "", err + } + + bytes, _ := os.ReadFile(msgFile.Name()) + content := string(bytes) + + if content == placeholder { + return "", nil + } + + return content, nil +}