Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Nemoden committed Feb 16, 2023
0 parents commit d939104
Show file tree
Hide file tree
Showing 11 changed files with 626 additions and 0 deletions.
23 changes: 23 additions & 0 deletions chat/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package chat

import (
"github.com/nemoden/chat/config"
gogpt "github.com/sashabaranov/go-gpt3"
)

type Client struct {
client *gogpt.Client
}

func NewClient(c *config.Config) (*Client, error) {
apiKey, _, err := config.LoadApiKey()
if err != nil {
return nil, err
}
client := gogpt.NewClient(apiKey)
return &Client{client}, nil
}

func (c *Client) GptClient() *gogpt.Client {
return c.client
}
46 changes: 46 additions & 0 deletions cmd/init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
Copyright © 2023 NAME HERE <EMAIL ADDRESS>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd

import (
"fmt"

"github.com/spf13/cobra"
)

// initCmd represents the init command
var initCmd = &cobra.Command{
Use: "init",
Short: "Initialises chat",
Long: "",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("init called")
},
}

func init() {
rootCmd.AddCommand(initCmd)

// Here you will define your flags and configuration settings.

// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// initCmd.PersistentFlags().String("foo", "", "A help for foo")

// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// initCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
17 changes: 17 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package cmd

import "github.com/spf13/cobra"

func GetRootCmd() *cobra.Command {
return rootCmd
}

func GetSubcommandsMap() map[string]bool {
commands := rootCmd.Commands()
commandsLen := len(commands)
subcommands := make(map[string]bool, commandsLen)
for _, c := range commands {
subcommands[c.Use] = true
}
return subcommands
}
110 changes: 110 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
Copyright © 2023 NAME HERE <EMAIL ADDRESS>
*/
package cmd

import (
"bufio"
"context"
"errors"
"fmt"
"strings"

"os"

"github.com/nemoden/chat/chat"
"github.com/nemoden/chat/config"
gogpt "github.com/sashabaranov/go-gpt3"

"github.com/spf13/cobra"
)

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "chat",
Short: "ChatGPT CLI and REPL",
Long: `Use ChatGPT not leaving your terminal`,
FParseErrWhitelist: cobra.FParseErrWhitelist{
UnknownFlags: true,
},
Run: func(cmd *cobra.Command, args []string) {
reader := bufio.NewReader(os.Stdin)

options := getOptions(os.Args[1:])
c := config.LoadConfig(options)
client, err := chat.NewClient(c)

if errors.Is(err, config.ErrApiTokenFileDoesntExist) {
fmt.Printf(config.TokenFileDoesntExistPrompt)
os.Exit(0)
}
if errors.Is(err, config.ErrCantOpenApiTokenFileForReading) {
fmt.Printf(config.TokenFileNotReadablePrompt)
os.Exit(0)
}

ctx := context.Background()

for {
fmt.Printf("You: ")
input, _ := reader.ReadString('\n')
input = strings.TrimSpace(input)

if input == "exit" {
break
}

prompt := ""
if c.Format == "markdown" {
prompt += "Return response in markdown format. Prompt on a new line:\n"
}
prompt += input
req := gogpt.CompletionRequest{
Model: gogpt.GPT3TextDavinci003,
Prompt: prompt,
MaxTokens: 1000,
Temperature: 0.5,
}

fmt.Printf("ChatGPT: ")

stream, _ := client.GptClient().CreateCompletionStream(ctx, req)

defer stream.Close()

c.Renderer.Render(stream)
}
},
}

// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}

func init() {
// Here you will define your flags and configuration settings.
// Cobra supports persistent flags, which, if defined here,
// will be global for your application.

// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.chat.yaml)")

// Cobra also supports local flags, which will only run
// when this action is called directly.
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

// @TODO DRY, duplication in ./main.go
func getOptions(args []string) []string {
var opts []string
for _, arg := range args {
if strings.HasPrefix(arg, "--") {
opts = append(opts, arg)
}
}
return opts
}
155 changes: 155 additions & 0 deletions config/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package config

import (
"bufio"
"errors"
"fmt"
"os"
"path"
"runtime"

"github.com/nemoden/chat/renderer"
)

var (
ConfigDir string = getConfigDir()
CacheDir string = getCacheDir()
ApiKeyFilePath string = getApiKeyFilePath()
InitPrompt string = `Please initialise chat using the init command:
$ chat init
`
// @TODO revisit instructions
TokenFileDoesntExistPrompt string = fmt.Sprintf(`Oops. Looks like your token file %s doesn't exist.
Please obtain the openai API key from their website https://platform.openai.com/account/api-keys
Once you have the API key, you can either add it manually to %s:
$ echo "<your-api-key>" > %s
Or if you didn't run chat init, it's adviced that you do this instead:
$ chat init
Alternatively, you can provide a token using environment variable %s. How to set it depends on your shell, i.e.
- for bash shell, add export %s=<your-api-key> to your ~/.bash_profile
- for zsh shell, add export %s=<your-api-key> to your ~/.zsh_profile
- for fish shell, add set -x %s <your-api-key> to your ~/.config/fish/config.fish
`, ApiKeyFilePath, ApiKeyFilePath, ApiKeyFilePath, API_KEY_ENV_VAR_NAME, API_KEY_ENV_VAR_NAME, API_KEY_ENV_VAR_NAME, API_KEY_ENV_VAR_NAME)

TokenFileNotReadablePrompt string = fmt.Sprintf(`Oops. Looks like your token file %s is not readable.
chat stores the api token in that file.
Another option is to store the API key in the environment variable. How to set it depends on your shell, i.e.
- for bash shell, add export %s=<your-api-key> to your ~/.bash_profile
- for zsh shell, add export %s=<your-api-key> to your ~/.zsh_profile
- for fish shell, add set -x %s <your-api-key> to your ~/.config/fish/config.fish
`, ApiKeyFilePath, API_KEY_ENV_VAR_NAME, API_KEY_ENV_VAR_NAME, API_KEY_ENV_VAR_NAME)
)

var (
ErrApiTokenFileDoesntExist = errors.New(fmt.Sprintf("API key file %s doesn't exist", ApiKeyFilePath))
ErrCantOpenApiTokenFileForReading = errors.New(fmt.Sprintf("Can not open API key file %s for reading", ApiKeyFilePath))
)

const (
APP_NAME = "chat"
API_KEY_ENV_VAR_NAME = "CHAT_GPT_API_TOKEN"
API_KEY_SOURCE_ENV = "env"
API_KEY_SOURCE_FILE = "file"
)

type Config struct {
Renderer renderer.Renderer
Format string
}

func inSlice(what string, slice []string) bool {
for _, i := range slice {
if what == i {
return true
}
}
return false
}
func LoadConfig(optionsOverride []string) *Config {
var r renderer.Renderer
var format string
if inSlice("--md", optionsOverride) {
r = renderer.NewMarkdownRenderer(os.Stdout, "ChatGPT: ")
format = "markdown"
} else {
r = renderer.NewPassthruRenderer(os.Stdout, "ChatGPT: ")
format = ""
}
return &Config{
Renderer: r,
Format: format,
}
}

func LoadApiKey() (string, string, error) {
apiKey := os.Getenv(API_KEY_ENV_VAR_NAME)
if apiKey != "" {
return apiKey, API_KEY_SOURCE_ENV, nil
}
_, err := os.Stat(ApiKeyFilePath)
if err != nil {
return "", "", ErrApiTokenFileDoesntExist
}
file, err := os.Open(ApiKeyFilePath)
if err != nil {
return "", "", ErrCantOpenApiTokenFileForReading
}
defer file.Close()

scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanWords)
scanner.Scan()
return scanner.Text(), API_KEY_SOURCE_FILE, nil
}

func getCacheDir() string {
var dir string
switch runtime.GOOS {
case "darwin", "linux", "freebsd", "openbsd":
dir = os.Getenv("XDG_CACHE_HOME")
if dir != "" {
return dir
}
home, _ := os.UserHomeDir()
return path.Join(home, ".cache", APP_NAME)
default:
dir, _ := os.UserConfigDir()
return dir
}
}

func getConfigDir() string {
var dir string
switch runtime.GOOS {
case "darwin", "linux", "freebsd", "openbsd":
dir = os.Getenv("XDG_CONFIG_HOME")
if dir != "" {
return dir
}
home, _ := os.UserHomeDir()
return path.Join(home, ".config", APP_NAME)
default:
dir, _ := os.UserConfigDir()
return dir
}
}

func getApiKeyFilePath() string {
home, _ := os.UserHomeDir()
return path.Join(home, "."+APP_NAME)
}

func StoreApiToken(token string) bool {
return true
}
29 changes: 29 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module github.com/nemoden/chat

go 1.19

require (
github.com/alecthomas/chroma v0.10.0 // indirect
github.com/aymanbagabas/go-osc52 v1.0.3 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/charmbracelet/glamour v0.6.0 // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/gosuri/uilive v0.0.4 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/microcosm-cc/bluemonday v1.0.21 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.13.0 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/sashabaranov/go-gpt3 v1.0.0 // indirect
github.com/spf13/cobra v1.6.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/yuin/goldmark v1.5.2 // indirect
github.com/yuin/goldmark-emoji v1.0.1 // indirect
golang.org/x/net v0.0.0-20221002022538-bcab6841153b // indirect
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect
)
Loading

0 comments on commit d939104

Please sign in to comment.