-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit d939104
Showing
11 changed files
with
626 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
Oops, something went wrong.