diff --git a/README.md b/README.md index 4c8ad00..6d16e9e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,52 @@ # chatgpt -CLI application for working with ChatGPT + +CLI application for working with ChatGPT. + +``` +go install github.com/verdverm/chatgpt@latest + +chatgpt -h +``` + +Examples: + +``` +Chat with ChatGPT in console. + +Examples: + # start an interactive session + chatgpt -i + + # ask chatgpt for a one-time response + chatgpt -q "answer me this ChatGPT..." + + # provide context to a question or conversation + chatgpt context.txt -i + chatgpt context.txt -q "answer me this ChatGPT..." + + # read context from file and write response back + chatgpt convo.txt + + # pipe content from another program, useful for ! in vim visual mode + cat convo.txt | chatgpt + + # inspect the predifined pretexts, which set ChatGPT's mood + chatgpt -p list + chatgpt -p view: + + # use a pretext with any of the previous modes + chatgpt -p optimistic -i + chatgpt -p cynic -q "Is the world going to be ok?" + chatgpt -p teacher convo.txt + +Usage: + chatgpt [file] [flags] + +Flags: + -h, --help help for chatgpt + -i, --interactive start an interactive session with ChatGPT + -p, --pretext string pretext to add to ChatGPT input, use 'list' or 'view:' to inspect predefined, '' to use a pretext, or otherwise supply any custom text + -q, --question string ask a single question and print the response back + -t, --tokens int set the MaxTokens to generate per response (default 420) +``` + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..07c0f4e --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module github.com/verdverm/chatgpt + +go 1.18 + +require ( + github.com/sashabaranov/go-gpt3 v1.0.1 + github.com/spf13/cobra v1.6.1 +) + +require ( + github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..78ec907 --- /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.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sashabaranov/go-gpt3 v1.0.1 h1:KHwY4uroFlX1qI1Hui7d31ZI6uzbNGL9zAkh1FkfhuM= +github.com/sashabaranov/go-gpt3 v1.0.1/go.mod h1:BIZdbwdzxZbCrcKGMGH6u2eyGe1xFuX9Anmh3tCP8lQ= +github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= +github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +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..de86bf8 --- /dev/null +++ b/main.go @@ -0,0 +1,252 @@ +package main + +import ( + "bufio" + "bytes" + "context" + "embed" + "fmt" + "os" + "strings" + + gpt3 "github.com/sashabaranov/go-gpt3" + "github.com/spf13/cobra" +) + +var LongHelp = ` +Chat with ChatGPT in console. + +Examples: + # start an interactive session + chatgpt -i + + # ask chatgpt for a one-time response + chatgpt -q "answer me this ChatGPT..." + + # provide context to a question or conversation + chatgpt context.txt -i + chatgpt context.txt -q "answer me this ChatGPT..." + + # read context from file and write response back + chatgpt convo.txt + + # pipe content from another program, useful for ! in vim visual mode + cat convo.txt | chatgpt + + # inspect the predifined pretexts, which set ChatGPT's mood + chatgpt -p list + chatgpt -p view: + + # use a pretext with any of the previous modes + chatgpt -p optimistic -i + chatgpt -p cynic -q "Is the world going to be ok?" + chatgpt -p teacher convo.txt +` + +//go:embed pretexts/* +var predefined embed.FS + +var Question string +var Pretext string +var MaxTokens int +var PromptMode bool +var PromptText string + +func GetResponse(client *gpt3.Client, ctx context.Context, question string) (string, error) { + req := gpt3.CompletionRequest{ + Model: gpt3.GPT3TextDavinci003, + MaxTokens: MaxTokens, + Prompt: question, + } + resp, err := client.CreateCompletion(ctx, req) + if err != nil { + return "", err + } + + return resp.Choices[0].Text, nil +} + +type NullWriter int + +func (NullWriter) Write([]byte) (int, error) { return 0, nil } + +func main() { + apiKey := os.Getenv("CHATGPT_API_KEY") + if apiKey == "" { + fmt.Println("CHATGPT_API_KEY environment var is missing\nVisit https://platform.openai.com/account/api-keys to get one\n") + os.Exit(1) + } + + client := gpt3.NewClient(apiKey) + + rootCmd := &cobra.Command{ + Use: "chatgpt [file]", + Short: "Chat with ChatGPT in console.", + Long: LongHelp, + Run: func(cmd *cobra.Command, args []string) { + var err error + var filename string + + if Pretext != "" { + + files, err := predefined.ReadDir("pretexts") + if err != nil { + panic(err) + } + + if Pretext == "list" { + for _, f := range files { + fmt.Println(strings.TrimSuffix(f.Name(), ".txt")) + } + os.Exit(0) + } + + if strings.HasPrefix(Pretext, "view:") { + name := strings.TrimPrefix(Pretext, "view:") + contents, err := predefined.ReadFile("pretexts/" + name + ".txt") + if err != nil { + fmt.Println(err) + os.Exit(1) + } + fmt.Println(string(contents)) + os.Exit(0) + } + + // look for predefined + for _, f := range files { + name := strings.TrimSuffix(f.Name(), ".txt") + if name == Pretext { + contents, err := predefined.ReadFile("pretexts/" + name + ".txt") + if err != nil { + fmt.Println(err) + os.Exit(1) + } + PromptText = string(contents) + break + } + } + + if PromptText == "" { + PromptText = Pretext + } + + } + + if len(args) == 0 && !PromptMode && Question == "" { + reader := bufio.NewReader(os.Stdin) + var buf bytes.Buffer + for { + b, err := reader.ReadByte() + if err != nil { + break + } + buf.WriteByte(b) + } + PromptText += buf.String() + } else if len(args) == 1 { + filename = args[0] + content, err := os.ReadFile(filename) + if err != nil { + fmt.Println(err) + return + } + PromptText += string(content) + } + + if Question != "" { + PromptText += "\n" + Question + } + + if PromptMode { + fmt.Println(PromptText) + err = RunPrompt(client) + } else { + err = RunOnce(client, filename) + } + + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + }, + } + + + rootCmd.Flags().StringVarP(&Question, "question", "q", "", "ask a single question and print the response back") + rootCmd.Flags().StringVarP(&Pretext, "pretext", "p", "", "pretext to add to ChatGPT input, use 'list' or 'view:' to inspect predefined, '' to use a pretext, or otherwise supply any custom text") + rootCmd.Flags().BoolVarP(&PromptMode, "interactive", "i", false, "start an interactive session with ChatGPT") + rootCmd.Flags().IntVarP(&MaxTokens, "tokens", "t", 420, "set the MaxTokens to generate per response") + + rootCmd.Execute() +} + +func RunPrompt(client *gpt3.Client) error { + ctx := context.Background() + scanner := bufio.NewScanner(os.Stdin) + quit := false + + for !quit { + fmt.Print("> ") + + if !scanner.Scan() { + break + } + + question := scanner.Text() + switch question { + case "quit", "q", "exit": + quit = true + + default: + PromptText += "\n\n> " + question + "\n" + r, err := GetResponse(client, ctx, PromptText) + if err != nil { + return err + } + + PromptText += "\n" + r + "\n" + fmt.Println(r + "\n") + } + } + + return nil +} + +func RunOnce(client *gpt3.Client, filename string) error { + ctx := context.Background() + + r, err := GetResponse(client, ctx, PromptText) + if err != nil { + return err + } + + if filename == "" { + fmt.Println(r) + } else { + err = AppendToFile(filename, r) + if err != nil { + return err + } + } + + return nil +} + +// AppendToFile provides a function to append data to an existing file, +// creating it if it doesn't exist +func AppendToFile(filename string, data string) error { + // Open the file in append mode + file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return err + } + + // Append the data to the file + _, err = file.WriteString(data) + if err != nil { + return err + } + + return file.Close() +} diff --git a/pretexts/coding.txt b/pretexts/coding.txt new file mode 100644 index 0000000..85842b7 --- /dev/null +++ b/pretexts/coding.txt @@ -0,0 +1,3 @@ +As an enthusiastic teachers assistant, respond to the following. +Provide context for the answer, then a markdown code block based on the +user's prompt, followed by a discussion to how the solution works. diff --git a/pretexts/cynic.txt b/pretexts/cynic.txt new file mode 100644 index 0000000..0fa206e --- /dev/null +++ b/pretexts/cynic.txt @@ -0,0 +1,2 @@ +As a cynical, gloomy person, respond to the following. +Talk about problems over solutions and present unpleasant outcomes. diff --git a/pretexts/optimistic.txt b/pretexts/optimistic.txt new file mode 100644 index 0000000..99a9414 --- /dev/null +++ b/pretexts/optimistic.txt @@ -0,0 +1,2 @@ +As a happy, optimistic person, respond to the following. +Focus on solutions over problems and present favorable outcomes. diff --git a/pretexts/teacher.txt b/pretexts/teacher.txt new file mode 100644 index 0000000..990dfa8 --- /dev/null +++ b/pretexts/teacher.txt @@ -0,0 +1,2 @@ +As an enthusiastic teacher, respond to the following. +Provide an answer discuss why it is correct. diff --git a/pretexts/thoughtful.txt b/pretexts/thoughtful.txt new file mode 100644 index 0000000..435c4a4 --- /dev/null +++ b/pretexts/thoughtful.txt @@ -0,0 +1,2 @@ +As a thoughtful, neutral person, respond the following. +Provide context and background. Discuss the pros, cons, and tradeoffs. diff --git a/test.txt b/test.txt new file mode 100644 index 0000000..4ab803d --- /dev/null +++ b/test.txt @@ -0,0 +1,2 @@ +> write some python to parse a timestamp +