Repository: ian-kent/gptchat
Branch: main
Commit: 21441696c2e2
Files: 24
Total size: 55.3 KB
Directory structure:
gitextract_eagp9m2d/
├── LICENSE.md
├── README.md
├── chat_loop.go
├── command.go
├── config/
│ └── config.go
├── conversation.go
├── go.mod
├── go.sum
├── main.go
├── module/
│ ├── memory/
│ │ ├── memory.go
│ │ ├── recall.go
│ │ ├── storage.go
│ │ └── store.go
│ ├── module.go
│ ├── plugin/
│ │ ├── compiled/
│ │ │ └── README.md
│ │ ├── create.go
│ │ └── source/
│ │ └── README.md
│ └── plugin.go
├── parser/
│ ├── cmd/
│ │ └── main.go
│ ├── parser.go
│ └── parser_test.go
├── ui/
│ ├── theme.go
│ └── ui.go
└── util/
└── strings.go
================================================
FILE CONTENTS
================================================
================================================
FILE: LICENSE.md
================================================
The MIT License (MIT)
Copyright (c) 2023 Ian Kent
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: README.md
================================================
GPTChat
=======
GPTChat is a client which gives GPT-4 some unique tools to be a better AI.
With GPTChat, GPT-4 can:
* remember useful information and recall it later
* recall information without knowing it's previously remembered it
* write it's own plugins and call them
* decide to write plugins without being prompted
* complete tasks by combining memories and plugins
* use multi-step commands to complete complex tasks
### Getting started
You'll need:
* A working installation of Go (which you download from https://go.dev/)
* An OpenAI account
* An API key with access to the GPT-4 API
If you don't have an API key, you can get one here:
https://platform.openai.com/account/api-keys
If you haven't joined the GPT-4 API waitlist, you can do that here:
https://openai.com/waitlist/gpt-4-api
Once you're ready:
1. Set the `OPENAI_API_KEY` environment variable to avoid the API key prompt on startup
2. Run GPTChat with `go run .` from the `gptchat` directory
3. Have fun!
## Memory
GPT-4's context window is pretty small.
GPTChat adds a long term memory which GPT-4 can use to remember useful information.
For example, if you tell GPT-4 what pets you have, it'll remember and can recall that information to answer questions even when the context is gone.
[See a GPT-4 memory demo on YouTube](https://www.youtube.com/watch?v=PUFZdM1nSTI)
## Plugins
GPT-4 can write its own plugins to improve itself.
For example, GPT-4 is pretty bad at math and generating random numbers.
With the plugin system, you can ask GPT-4 to generate two random numbers and add them together, and it'll write a plugin to do just that.
[See a GPT-4 plugin demo on YouTube](https://www.youtube.com/watch?v=o7M-XH6tMhc)
ℹ️ Plugins are only supported on unix based systems like Linux and MacOS - to get plugins working on Windows, you'll need to use something like WSL2.
## Contributing
PRs to add new features are welcome.
Be careful of prompt changes - small changes can disrupt GPT-4's ability to use the commands correctly.
## Disclaimer
You should supervise GPT-4's activity.
In one experiment, GPT-4 gave itself internet access with a HTTP client plugin - this seemed like a bad idea.
### Supervised mode
GPTChat will run in supervised mode by default.
This doesn't restrict any functionality, but does require user confirmation before compiling and executing any plugin code written by GPT, giving users a chance to review the code for safety before executing it.
⚠️ Code written by GPT is untrusted code from the internet and potentially dangerous
All code is compiled and executed as your user, with the same level of permissions your user has. It may be safer to run this in a container or virtual machine.
Supervised mode can be disabled but I wouldn't recommend it.
# License
See [LICENSE.md](LICENSE.md) for more information.
Copyright (c) 2023 Ian Kent
================================================
FILE: chat_loop.go
================================================
package main
import (
"context"
"fmt"
"strings"
"time"
"github.com/ian-kent/gptchat/config"
"github.com/ian-kent/gptchat/module"
"github.com/ian-kent/gptchat/parser"
"github.com/ian-kent/gptchat/ui"
"github.com/ian-kent/gptchat/util"
"github.com/sashabaranov/go-openai"
)
func chatLoop(cfg config.Config) {
RESET:
appendMessage(openai.ChatMessageRoleSystem, systemPrompt)
if cfg.IsDebugMode() {
ui.PrintChatDebug(ui.System, systemPrompt)
}
var skipUserInput = true
appendMessage(openai.ChatMessageRoleUser, openingPrompt)
if cfg.IsDebugMode() {
ui.PrintChatDebug(ui.User, openingPrompt)
}
if !cfg.IsDebugMode() {
ui.PrintChat(ui.App, "Setting up the chat environment, please wait for GPT to respond - this may take a few moments.")
}
var i int
for {
i++
if !skipUserInput {
input := ui.PromptChatInput()
var echo bool
ok, result := parseSlashCommand(input)
if ok {
// the command was handled but returned nothing
// to send to the AI, let's prompt the user again
if result == nil {
continue
}
if result.resetConversation {
resetConversation()
goto RESET
}
// if the result is a retry, we can just send the
// same request to GPT again
if result.retry {
skipUserInput = true
goto RETRY
}
if result.toggleDebugMode {
cfg = cfg.WithDebugMode(!cfg.IsDebugMode())
module.UpdateConfig(cfg)
if cfg.IsDebugMode() {
ui.PrintChat(ui.App, "Debug mode is now enabled")
} else {
ui.PrintChat(ui.App, "Debug mode is now disabled")
}
continue
}
if result.toggleSupervisedMode {
cfg = cfg.WithSupervisedMode(!cfg.IsSupervisedMode())
module.UpdateConfig(cfg)
if cfg.IsSupervisedMode() {
ui.PrintChat(ui.App, "Supervised mode is now enabled")
} else {
ui.PrintChat(ui.App, "Supervised mode is now disabled")
}
continue
}
// we have a prompt to give to the AI, let's do that
if result.prompt != "" {
input = result.prompt
echo = true
}
}
if echo {
ui.PrintChat(ui.User, input)
echo = false
}
appendMessage(openai.ChatMessageRoleUser, input)
}
skipUserInput = false
RETRY:
// Occasionally include the interval prompt
if i%5 == 0 {
interval := intervalPrompt()
appendMessage(openai.ChatMessageRoleSystem, interval)
if cfg.IsDebugMode() {
ui.PrintChatDebug(ui.System, interval)
}
}
attempts := 1
RATELIMIT_RETRY:
resp, err := client.CreateChatCompletion(
context.Background(),
openai.ChatCompletionRequest{
Model: cfg.OpenAIAPIModel(),
Messages: conversation,
},
)
if err != nil {
if strings.HasPrefix(err.Error(), "error, status code: 429") && attempts < 5 {
attempts++
ui.Error("rate limited, trying again in 1 second", err)
time.Sleep(time.Second)
goto RATELIMIT_RETRY
}
ui.Error("ChatCompletion failed", err)
if ui.PromptConfirm("Would you like to try again?") {
goto RATELIMIT_RETRY
}
continue
}
response := resp.Choices[0].Message.Content
appendMessage(openai.ChatMessageRoleAssistant, response)
if cfg.IsDebugMode() {
ui.PrintChat(ui.AI, response)
}
parseResult := parser.Parse(response)
if !cfg.IsDebugMode() && parseResult.Chat != "" {
ui.PrintChat(ui.AI, parseResult.Chat)
}
for _, command := range parseResult.Commands {
ok, result := module.ExecuteCommand(command.Command, command.Args, command.Body)
if ok {
// we had at least one AI command so we're going to respond automatically,
// no need to ask for user input
skipUserInput = true
if result.Error != nil {
msg := fmt.Sprintf(`An error occurred executing your command.
The command was:
`+util.TripleQuote+`
%s
`+util.TripleQuote+`
The error was:
`+util.TripleQuote+`
%s
`+util.TripleQuote, command.String(), result.Error.Error())
if result.Prompt != "" {
msg += fmt.Sprintf(`
The command provided this additional output:
`+util.TripleQuote+`
%s
`+util.TripleQuote, result.Prompt)
}
appendMessage(openai.ChatMessageRoleSystem, msg)
if cfg.IsDebugMode() {
ui.PrintChatDebug(ui.Module, msg)
}
continue
}
commandResult := fmt.Sprintf(`Your command returned some output.
The command was:
`+util.TripleQuote+`
%s
`+util.TripleQuote+`
The output was:
%s`, command.String(), result.Prompt)
appendMessage(openai.ChatMessageRoleSystem, commandResult)
if cfg.IsDebugMode() {
ui.PrintChatDebug(ui.Module, commandResult)
}
continue
}
}
}
}
================================================
FILE: command.go
================================================
package main
import (
"fmt"
"github.com/ian-kent/gptchat/ui"
"os"
"strings"
)
type slashCommandResult struct {
// the prompt to send to the AI
prompt string
// retry tells the client to resend the most recent conversation
retry bool
// resetConversation will reset the conversation to its original state,
// forgetting the conversation history
resetConversation bool
// toggleDebugMode will switch between debug on and debug off
toggleDebugMode bool
// toggleSupervisedMode will switch between supervised mode on and off
toggleSupervisedMode bool
}
type slashCommand struct {
command string
fn func(string) (bool, *slashCommandResult)
}
var slashCommands = []slashCommand{
{
command: "exit",
fn: func(s string) (bool, *slashCommandResult) {
os.Exit(0)
return true, nil
},
},
{
command: "retry",
fn: func(s string) (bool, *slashCommandResult) {
return true, &slashCommandResult{
retry: true,
}
},
},
{
command: "reset",
fn: func(s string) (bool, *slashCommandResult) {
return true, &slashCommandResult{
resetConversation: true,
}
},
},
{
command: "debug",
fn: func(s string) (bool, *slashCommandResult) {
return true, &slashCommandResult{
toggleDebugMode: true,
}
},
},
{
command: "supervisor",
fn: func(s string) (bool, *slashCommandResult) {
return true, &slashCommandResult{
toggleSupervisedMode: true,
}
},
},
{
command: "example",
fn: exampleCommand,
},
}
func helpCommand(string) (bool, *slashCommandResult) {
result := "The following commands are available:\n"
for _, e := range slashCommands {
result += fmt.Sprintf("\n /%s", e.command)
}
ui.PrintChat(ui.App, result)
return true, nil
}
func parseSlashCommand(input string) (ok bool, result *slashCommandResult) {
if !strings.HasPrefix(input, "/") {
return false, nil
}
input = strings.TrimPrefix(input, "/")
if input == "help" {
return helpCommand(input)
}
parts := strings.SplitN(input, " ", 2)
var cmd, args string
cmd = parts[0]
if len(parts) > 1 {
args = parts[1]
}
for _, command := range slashCommands {
if command.command == cmd {
return command.fn(args)
}
}
return false, nil
}
type example struct {
id, prompt string
}
var examples = []example{
{
id: "1",
prompt: "I want you to generate 5 random numbers and add them together.",
},
{
id: "2",
prompt: "I want you to generate 5 random numbers. Multiply the first and second number, then add the result to the remaining numbers.",
},
{
id: "3",
prompt: "I want you to generate 2 random numbers. Add them together then multiply the result by -1.",
},
{
id: "4",
prompt: "Can you summarise the tools you have available?",
},
{
id: "5",
prompt: "Can you suggest a task which might somehow use all of the available tools?",
},
}
func exampleCommand(args string) (bool, *slashCommandResult) {
for _, e := range examples {
if e.id == args {
return true, &slashCommandResult{
prompt: e.prompt,
}
}
}
result := "The following examples are available:"
for _, e := range examples {
result += fmt.Sprintf("\n\n/example %s\n %s", e.id, e.prompt)
}
ui.PrintChat(ui.App, result)
return true, nil
}
================================================
FILE: config/config.go
================================================
package config
type Config struct {
openaiAPIKey string
openaiAPIModel string
supervisedMode bool
debugMode bool
}
func New() Config {
return Config{
openaiAPIKey: "",
openaiAPIModel: "",
supervisedMode: true,
debugMode: false,
}
}
func (c Config) OpenAIAPIModel() string {
return c.openaiAPIModel
}
func (c Config) OpenAIAPIKey() string {
return c.openaiAPIKey
}
func (c Config) IsSupervisedMode() bool {
return c.supervisedMode
}
func (c Config) IsDebugMode() bool {
return c.debugMode
}
func (c Config) WithOpenAIAPIKey(apiKey string) Config {
c.openaiAPIKey = apiKey
return c
}
func (c Config) WithSupervisedMode(supervisedMode bool) Config {
c.supervisedMode = supervisedMode
return c
}
func (c Config) WithDebugMode(debugMode bool) Config {
c.debugMode = debugMode
return c
}
func (c Config) WithOpenAIAPIModel(apiModel string) Config {
c.openaiAPIModel = apiModel
return c
}
================================================
FILE: conversation.go
================================================
package main
import (
"fmt"
"github.com/ian-kent/gptchat/util"
"github.com/sashabaranov/go-openai"
"time"
)
var systemPrompt = `You are a helpful assistant.
You enjoy conversations with the user and like asking follow up questions to gather more information.
You have commands available which you can use to help me.
You can call these commands using the slash command syntax, for example, this is how you call the help command:
` + util.TripleQuote + `
/help
` + util.TripleQuote + `
The /help command will give you a list of the commands you have available.
Commands can also include a request body, for example, this is an example of a command which takes an input:
` + util.TripleQuote + `
/example
{
"expr": "value"
}
` + util.TripleQuote + `
Most commands also have subcommands, and this is an example of how you call a subcommand:
` + util.TripleQuote + `
/example subcommand
{
"expr": "value"
}
` + util.TripleQuote + `
To call a command, include the command in your response. You don't need to explain the command response to me, I don't care what it is, I only care that you can use it's output to follow my instructions.`
const openingPrompt = `Hello! Please familiarise yourself with the commands you have available.
You must do this before we have a conversation.`
func intervalPrompt() string {
return fmt.Sprintf(`The current date and time is %s.
Remember that the '/help' command will tell you what commands you have available.`, time.Now().Format("02 January 2006, 03:04pm"))
}
var conversation []openai.ChatCompletionMessage
func appendMessage(role string, message string) {
conversation = append(conversation, openai.ChatCompletionMessage{
Role: role,
Content: message,
})
}
func resetConversation() {
conversation = []openai.ChatCompletionMessage{}
}
================================================
FILE: go.mod
================================================
module github.com/ian-kent/gptchat
go 1.18
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.7.0 // indirect
github.com/dop251/goja v0.0.0-20230304130813-e2f543bf4b4c // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sashabaranov/go-openai v1.5.7 // indirect
github.com/spf13/cobra v1.6.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/testify v1.8.2 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/text v0.3.8 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: go.sum
================================================
github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
github.com/dop251/goja v0.0.0-20230304130813-e2f543bf4b4c h1:/utv6nmTctV6OVgfk5+O6lEMEWL+6KJy4h9NZ5fnkQQ=
github.com/dop251/goja v0.0.0-20230304130813-e2f543bf4b4c/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U=
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sashabaranov/go-openai v1.5.7 h1:8DGgRG+P7yWixte5j720y6yiXgY3Hlgcd0gcpHdltfo=
github.com/sashabaranov/go-openai v1.5.7/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
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=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: main.go
================================================
package main
import (
"fmt"
"os"
"strconv"
"strings"
"github.com/ian-kent/gptchat/config"
"github.com/ian-kent/gptchat/module"
"github.com/ian-kent/gptchat/module/memory"
"github.com/ian-kent/gptchat/module/plugin"
"github.com/ian-kent/gptchat/ui"
openai "github.com/sashabaranov/go-openai"
)
var client *openai.Client
var cfg = config.New()
func init() {
openaiAPIKey := strings.TrimSpace(os.Getenv("OPENAI_API_KEY"))
if openaiAPIKey == "" {
ui.Warn("You haven't configured an OpenAI API key")
fmt.Println()
if !ui.PromptConfirm("Do you have an API key?") {
ui.Warn("You'll need an API key to use GPTChat")
fmt.Println()
fmt.Println("* You can get an API key at https://platform.openai.com/account/api-keys")
fmt.Println("* You can get join the GPT-4 API waitlist at https://openai.com/waitlist/gpt-4-api")
os.Exit(1)
}
openaiAPIKey = ui.PromptInput("Enter your API key:")
if openaiAPIKey == "" {
fmt.Println("")
ui.Warn("You didn't enter an API key.")
os.Exit(1)
}
}
cfg = cfg.WithOpenAIAPIKey(openaiAPIKey)
openaiAPIModel := strings.TrimSpace(os.Getenv("OPENAI_API_MODEL"))
if openaiAPIModel == "" {
ui.Warn("You haven't configured an OpenAI API model, defaulting to GPT4")
openaiAPIModel = openai.GPT4
}
cfg = cfg.WithOpenAIAPIModel(openaiAPIModel)
supervisorMode := os.Getenv("GPTCHAT_SUPERVISOR")
switch strings.ToLower(supervisorMode) {
case "disabled":
ui.Warn("Supervisor mode is disabled")
cfg = cfg.WithSupervisedMode(false)
default:
}
debugEnv := os.Getenv("GPTCHAT_DEBUG")
if debugEnv != "" {
v, err := strconv.ParseBool(debugEnv)
if err != nil {
ui.Warn(fmt.Sprintf("error parsing GPT_DEBUG: %s", err.Error()))
} else {
cfg = cfg.WithDebugMode(v)
}
}
client = openai.NewClient(openaiAPIKey)
module.Load(cfg, client, []module.Module{
&memory.Module{},
&plugin.Module{},
}...)
if err := module.LoadCompiledPlugins(); err != nil {
ui.Warn(fmt.Sprintf("error loading compiled plugins: %s", err))
}
}
func main() {
ui.Welcome(
`Welcome to the GPT client.`,
`You can talk directly to GPT, or you can use /commands to interact with the client.
Use /help to see a list of available commands.`)
chatLoop(cfg)
}
================================================
FILE: module/memory/memory.go
================================================
package memory
import (
"errors"
"fmt"
"github.com/ian-kent/gptchat/config"
"github.com/ian-kent/gptchat/util"
openai "github.com/sashabaranov/go-openai"
)
type memory struct {
DateStored string `json:"date_stored"`
Memory string `json:"memory"`
}
type Module struct {
cfg config.Config
client *openai.Client
memories []memory
}
func (m *Module) ID() string {
return "memory"
}
func (m *Module) Load(cfg config.Config, client *openai.Client) error {
m.cfg = cfg
m.client = client
return m.loadFromFile()
}
func (m *Module) UpdateConfig(cfg config.Config) {
m.cfg = cfg
}
func (m *Module) Execute(args, body string) (string, error) {
switch args {
case "store":
return m.Store(body)
case "recall":
return m.Recall(body)
default:
return "", errors.New(fmt.Sprintf("command not implemented: /memory %s", args))
}
}
func (m *Module) Prompt() string {
return memoryPrompt
}
const memoryPrompt = `You also have a working long term memory.
You can remember something using the '/memory store' command, or you can recall it using the '/memory recall' command.
For example, if you want to store a fact I give you, you might reply with a message such as:
` + util.TripleQuote + `
/memory store {
"memory": "I bought cookies yesterday",
"context": "The user was discussing what they'd eaten"
}
` + util.TripleQuote + `
You should always include useful context in the memory to help with recall later, for example any relevant parts of the conversation which might help you remember why you stored a particular memory.
Later, you may try to recall this memory. You only need to use related concepts, since of course until you call '/memory recall' you cannot possibly know the memory you've stored. For example:
` + util.TripleQuote + `
/memory recall {
When did I buy cookies?
}
` + util.TripleQuote + `
or, another example might be:
` + util.TripleQuote + `
/memory recall {
What did I buy yesterday?
}
` + util.TripleQuote + `
This means you can remember interesting facts about our conversation. You should always consult your long term memory using the '/memory recall' command if you think I'm asking about something you don't have information about. For example, if I ask you "What did I eat for lunch yesterday?", and you don't know the answer, you must try to retrieve the answer from your long term memory.
You can use '/memory store' and '/memory recall' at any time. You might find this useful if I ask you to remember something. The commands must be an entire message, with no conversational text, and no quotes.
If I give you information that you don't already know, you must store this information in your memory. This could be any sort of information, for example a story, information about myself, people I know, things I've done, real world events. If you're unsure, you should store it just in case.
You must not remember the current date. The current date changes and is not a useful memory.`
================================================
FILE: module/memory/recall.go
================================================
package memory
import (
"context"
"encoding/json"
"github.com/ian-kent/gptchat/util"
"github.com/sashabaranov/go-openai"
)
func (m *Module) Recall(input string) (string, error) {
b, err := json.Marshal(m.memories)
if err != nil {
return "", err
}
resp, err := m.client.CreateChatCompletion(
context.Background(),
openai.ChatCompletionRequest{
Model: m.cfg.OpenAIAPIModel(),
Messages: []openai.ChatCompletionMessage{
{
Role: openai.ChatMessageRoleSystem,
Content: `You are a helpful assistant.
I'll give you a list of existing memories, and a prompt which asks you to identify the memory I'm looking for.
You should review the listed memories and suggest which memories might match the request.`,
},
{
Role: openai.ChatMessageRoleSystem,
Content: `Here are your memories in JSON format:
` + util.TripleQuote + `
` + string(b) + `
` + util.TripleQuote,
},
{
Role: openai.ChatMessageRoleSystem,
Content: `Help me find any memories which may match this request:
` + util.TripleQuote + `
` + input + `
` + util.TripleQuote,
},
},
},
)
if err != nil {
return "", err
}
response := resp.Choices[0].Message.Content
return `You have successfully recalled this memory:
` + util.TripleQuote + `
` + response + `
` + util.TripleQuote, nil
// TODO find a prompt which gets GPT to adjust relative time
// return `You have successfully recalled this memory:
//
//` + util.TripleQuote + `
//` + response + `
//` + util.TripleQuote + `
//
//If this memory mentions relative time (for example today, yesterday, last week, tomorrow), remember to take this into consideration when using this information to answer questions.
//
//For example, if the memory says "tomorrow" and the memory was stored on 25th, the memory is actually referring to 26th.`, nil
}
================================================
FILE: module/memory/storage.go
================================================
package memory
import (
"encoding/json"
"io/ioutil"
"os"
)
func (m *Module) loadFromFile() error {
_, err := os.Stat("memories.json")
if os.IsNotExist(err) {
return nil
}
b, err := ioutil.ReadFile("memories.json")
if err != nil {
return err
}
err = json.Unmarshal(b, &m.memories)
if err != nil {
return err
}
return nil
}
func (m *Module) writeToFile() error {
b, err := json.Marshal(m.memories)
if err != nil {
return err
}
err = ioutil.WriteFile("memories.json", b, 0660)
if err != nil {
return err
}
return nil
}
func (m *Module) appendMemory(mem memory) error {
m.memories = append(m.memories, mem)
return m.writeToFile()
}
================================================
FILE: module/memory/store.go
================================================
package memory
import (
"github.com/ian-kent/gptchat/util"
"time"
)
func (m *Module) Store(input string) (string, error) {
err := m.appendMemory(memory{
DateStored: time.Now().Format("02 January 2006, 03:04pm"),
Memory: input,
})
if err != nil {
return "", err
}
return `You have successfully stored this memory:
` + util.TripleQuote + `
` + input + `
` + util.TripleQuote, nil
}
================================================
FILE: module/module.go
================================================
package module
import (
"errors"
"fmt"
"github.com/ian-kent/gptchat/config"
"github.com/ian-kent/gptchat/ui"
openai "github.com/sashabaranov/go-openai"
"strings"
)
type Module interface {
Load(config.Config, *openai.Client) error
UpdateConfig(config.Config)
ID() string
Prompt() string
Execute(args, body string) (string, error)
}
// IntervalPrompt allows a module to inject a prompt into the interval prompt
type IntervalPrompt interface {
IntervalPrompt() string
}
var loadedModules = make(map[string]Module)
func Load(cfg config.Config, client *openai.Client, modules ...Module) error {
for _, module := range modules {
if err := module.Load(cfg, client); err != nil {
ui.Warn(fmt.Sprintf("failed to load module %s: %s", module.ID(), err))
continue
}
if cfg.IsDebugMode() {
ui.Info(fmt.Sprintf("loaded module %s", module.ID()))
}
loadedModules[module.ID()] = module
}
return nil
}
func UpdateConfig(cfg config.Config) {
for _, module := range loadedModules {
_, ok := module.(pluginLoader)
if ok {
// GPT written plugins shouldn't have config, nothing to do
continue
}
module.UpdateConfig(cfg)
}
}
func IsLoaded(id string) bool {
_, ok := loadedModules[id]
return ok
}
func LoadPlugin(m Module) error {
// a plugin doesn't have access to the openai client so it's safe to pass in nil here
//
// we also don't pass in the config since it may contain sensitive information that
// we don't want GPT to have access to
return Load(config.Config{}, nil, m)
}
type CommandResult struct {
Error error
Prompt string
}
func HelpCommand() (bool, *CommandResult) {
result := "Here are the commands you have available:\n\n"
for _, mod := range loadedModules {
result += fmt.Sprintf(" * /%s\n", mod.ID())
}
result += `
You can call commands using the /command syntax.
Calling a command without any additional arguments will explain it's usage. You should do this to learn how the command works.`
return true, &CommandResult{
Prompt: result,
}
}
func ExecuteCommand(command, args, body string) (bool, *CommandResult) {
if command == "/help" {
return HelpCommand()
}
cmd := strings.TrimPrefix(command, "/")
mod, ok := loadedModules[cmd]
if !ok {
return true, &CommandResult{
Error: errors.New(fmt.Sprintf("Unrecognised command: %s", command)),
}
}
if args == "" && body == "" {
return true, &CommandResult{
Prompt: mod.Prompt(),
}
}
res, err := mod.Execute(args, body)
if err != nil {
return true, &CommandResult{
Error: err,
}
}
return true, &CommandResult{
Prompt: res,
}
}
================================================
FILE: module/plugin/compiled/README.md
================================================
Compiled plugins will be added to this directory.
================================================
FILE: module/plugin/create.go
================================================
package plugin
import (
"errors"
"fmt"
"github.com/fatih/color"
"github.com/ian-kent/gptchat/config"
"github.com/ian-kent/gptchat/module"
"github.com/ian-kent/gptchat/ui"
"github.com/ian-kent/gptchat/util"
openai "github.com/sashabaranov/go-openai"
"io/ioutil"
"os"
"os/exec"
"strings"
)
var (
// TODO make this configurable
PluginSourcePath = "./module/plugin/source"
PluginCompilePath = "./module/plugin/compiled"
)
var ErrPluginSourcePathMissing = errors.New("plugin source path is missing")
var ErrPluginCompilePathMissing = errors.New("plugin compiled path is missing")
func CheckPaths() error {
_, err := os.Stat(PluginSourcePath)
if err != nil && !os.IsNotExist(err) {
return err
}
if err != nil {
return ErrPluginSourcePathMissing
}
_, err = os.Stat(PluginCompilePath)
if err != nil && !os.IsNotExist(err) {
return err
}
if err != nil {
return ErrPluginCompilePathMissing
}
return nil
}
type Module struct {
cfg config.Config
client *openai.Client
}
func (m *Module) Load(cfg config.Config, client *openai.Client) error {
m.cfg = cfg
m.client = client
if err := CheckPaths(); err != nil {
return err
}
return nil
}
func (m *Module) UpdateConfig(cfg config.Config) {
m.cfg = cfg
}
func (m *Module) Prompt() string {
return newPluginPrompt
}
func (m *Module) ID() string {
return "plugin"
}
func (m *Module) Execute(args, body string) (string, error) {
parts := strings.SplitN(args, " ", 2)
cmd := parts[0]
if len(parts) > 1 {
args = parts[1]
}
switch cmd {
case "create":
return m.createPlugin(args, body)
default:
return "", errors.New(fmt.Sprintf("%s not implemented", args))
}
}
func (m *Module) createPlugin(id, body string) (string, error) {
body = strings.TrimSpace(body)
if len(body) == 0 {
return "", errors.New("plugin source not found")
}
if !strings.HasPrefix(body, "{") || !strings.HasSuffix(body, "}") {
return "", errors.New("plugin source must be between {} in '/plugin create plugin-id {}' command")
}
id = strings.TrimSpace(id)
if id == "" {
return "", errors.New("plugin id is invalid")
}
if module.IsLoaded(id) {
return "", errors.New("a plugin with this id already exists")
}
source := strings.TrimPrefix(strings.TrimSuffix(body, "}"), "{")
pluginSourceDir := PluginSourcePath + "/" + id
_, err := os.Stat(pluginSourceDir)
if err != nil && !os.IsNotExist(err) {
return "", fmt.Errorf("error checking if directory exists: %s", err)
}
// only create the directory if it doesn't exist; it's possible GPT had a compile
// error in the last attempt in which case we can overwrite it
//
// we don't get this if the last attempt was successful since it'll show up
// as a loaded plugin in the check above
if os.IsNotExist(err) {
// if err is nil then the directory doesn't exist, let's create it
err := os.Mkdir(pluginSourceDir, 0777)
if err != nil {
return "", fmt.Errorf("error creating directory: %s", err)
}
}
sourcePath := pluginSourceDir + "/plugin.go"
err = ioutil.WriteFile(sourcePath, []byte(source), 0644)
if err != nil {
return "", fmt.Errorf("error writing source file: %s", err)
}
if m.cfg.IsSupervisedMode() {
fmt.Println("============================================================")
fmt.Println()
ui.Warn("⚠️ GPT written plugins are untrusted code from the internet")
fmt.Println()
fmt.Println("You should review this code before allowing it to be compiled and executed.")
fmt.Println()
fmt.Println("If you allow this action, GPT is able to execute code with the same permissions as your user.")
fmt.Println()
color.New(color.FgHiWhite, color.Bold).Println("This is potentially dangerous.")
fmt.Println()
fmt.Println("The source code GPT has written can be found here:")
fmt.Println(sourcePath)
fmt.Println()
confirmation := ui.PromptInput("Enter 'confirm' to confirm, anything else will block:")
if confirmation != "confirm" {
fmt.Println()
fmt.Println("============================================================")
return "The user has prevented you from running this code", errors.New(confirmation)
}
fmt.Println()
fmt.Println("============================================================")
fmt.Println()
}
pluginPath := PluginCompilePath + "/" + id + ".so"
cmd := exec.Command("go", "build", "-buildmode=plugin", "-o", pluginPath, sourcePath)
if b, err := cmd.CombinedOutput(); err != nil {
return string(b), fmt.Errorf("error compiling plugin: %s", err)
}
loadedPlugin, err := module.OpenPlugin(pluginPath)
if err != nil {
return "", fmt.Errorf("error opening plugin: %s", err)
}
// Call the functions provided by the plugin
compiledID := loadedPlugin.ID()
if id != compiledID {
return "", errors.New("ID() does not return the ID specified in the '/plugin create <plugin-id>' command")
}
err = module.LoadPlugin(module.GetModuleForPlugin(loadedPlugin))
if err != nil {
return "", fmt.Errorf("error loading plugin: %s", err)
}
return `Great! Your plugin has been loaded successfully.
Why don't you check the /help command to see if your new plugin is available.'`, nil
}
var newPluginPrompt = `You can add new plugins which you can call using a slash command.
They're written in Go, so all you need to do is create a new struct which implements the correct interface.
The interface you need to implement is:
` + util.TripleQuote + `
type Plugin interface {
Example() string
Execute(input map[string]any) (map[string]any, error)
}
` + util.TripleQuote + `
You don't need to write any supporting code like the main function, you only need to implement the struct.
Here's the full code for the "add 1" plugin you can use to guide your output:
` + util.TripleQuote + `
package main
import "github.com/ian-kent/gptchat/module"
var Plugin module.Plugin = AddOne{}
type AddOne struct{}
func (c AddOne) ID() string {
return "add-one"
}
func (c AddOne) Example() string {
return ` + util.SingleQuote + `/add-one {
"value": 5
}` + util.SingleQuote + `
}
func (c AddOne) Execute(input map[string]any) (map[string]any, error) {
value, ok := input["value"].(int)
if !ok {
return nil, nil
}
value = value + 1
return map[string]any{
"result": value,
}, nil
}
` + util.TripleQuote + `
It's best if the plugins you create don't have any external dependencies. You can call external APIs if you want to, but you should avoid APIs which require authentication since you won't have the required access.
Your plugin must import the module package and must define a package variable named 'Plugin', just like with the AddOne example. The result of the Execute function you implement must return either a value or an error.
The input to Execute is a map[string]any which you should assume is unmarshaled from JSON. This means you must use appropriate data types, for example a float64 when working with numbers.
To create a plugin, you should use the "/plugin create <plugin-id> {}" command, for example:
` + util.TripleQuote + `
/plugin create add-one {
package main
// the rest of your plugin source here
}
` + util.TripleQuote + `
Your code inside the '/plugin create' body must be valid Go code which can compile without any errors. Do not include quotes or attempt to use a JSON body.`
================================================
FILE: module/plugin/source/README.md
================================================
Plugin source will be added to this directory.
================================================
FILE: module/plugin.go
================================================
package module
import (
"encoding/json"
"errors"
"fmt"
"github.com/ian-kent/gptchat/config"
"github.com/ian-kent/gptchat/ui"
"github.com/sashabaranov/go-openai"
"os"
"plugin"
"strings"
)
type Plugin interface {
ID() string
Example() string
Execute(map[string]any) (map[string]any, error)
}
type pluginLoader struct {
plugin Plugin
}
func (p pluginLoader) Load(config.Config, *openai.Client) error {
return nil
}
func (p pluginLoader) UpdateConfig(config.Config) {}
func (p pluginLoader) ID() string {
return p.plugin.ID()
}
func (p pluginLoader) Prompt() string {
return p.plugin.Example()
}
func (p pluginLoader) Execute(args, body string) (string, error) {
input := make(map[string]any)
if body != "" {
err := json.Unmarshal([]byte(body), &input)
if err != nil {
return "", fmt.Errorf("plugin body must be valid json: %s", err)
}
}
output, err := p.plugin.Execute(input)
if err != nil {
return "", fmt.Errorf("error executing plugin: %s", err)
}
b, err := json.Marshal(output)
if err != nil {
return "", fmt.Errorf("error converting plugin output to json: %s", err)
}
return string(b), nil
}
func GetModuleForPlugin(p Plugin) Module {
return pluginLoader{p}
}
func LoadCompiledPlugins() error {
pluginPath := "./module/plugin/compiled/"
entries, err := os.ReadDir(pluginPath)
if err != nil {
return fmt.Errorf("error loading compiled plugins: %s", err)
}
for _, entry := range entries {
if !strings.HasSuffix(entry.Name(), ".so") {
continue
}
loadedPlugin, err := OpenPlugin(pluginPath + entry.Name())
if err != nil {
ui.Warn(fmt.Sprintf("error opening plugin: %s", err))
continue
}
pluginID := loadedPlugin.ID()
if IsLoaded(pluginID) {
ui.Warn(fmt.Sprintf("plugin with this ID is already loaded: %s", err))
continue
}
err = LoadPlugin(GetModuleForPlugin(loadedPlugin))
if err != nil {
ui.Warn(fmt.Sprintf("error loading plugin: %s", err))
continue
}
}
return nil
}
func OpenPlugin(path string) (Plugin, error) {
p, err := plugin.Open(path)
if err != nil {
return nil, fmt.Errorf("error loading plugin: %s", err)
}
apiSymbol, err := p.Lookup("Plugin")
if err != nil {
return nil, fmt.Errorf("error finding plugin implementation: %s", err)
}
// Cast the symbol to the ScriptAPI interface
api, ok := apiSymbol.(*Plugin)
if !ok {
return nil, errors.New("plugin does not implement the Plugin interface")
}
loadedPlugin := *api
return loadedPlugin, nil
}
================================================
FILE: parser/cmd/main.go
================================================
package main
import (
"fmt"
"github.com/ian-kent/gptchat/parser"
"strings"
)
func main() {
input := `/plugins create my-plugin
{
package main
import "fmt"
func main() {
fmt.Println("test")
}
}`
tokens := parser.Lex(input)
fmt.Println("Tokens:")
for _, token := range tokens {
fmt.Printf(" %20s => %s\n", token.Typ, token.Val)
}
fmt.Println()
result := parser.ParseTokens(tokens)
fmt.Println("Result:")
fmt.Println(" Chat:")
fmt.Println(indent(result.Chat, " "))
fmt.Println(" Commands:")
for _, command := range result.Commands {
fmt.Printf(" - Command: %s\n", command.Command)
fmt.Printf(" - Args: %s\n", command.Args)
fmt.Printf(" - Body:\n")
fmt.Println(indent(command.Body, " "))
}
}
func indent(input string, prefix string) string {
lines := strings.Split(string(input), "\n")
var output string
for _, line := range lines {
output += prefix + line + "\n"
}
return output
}
================================================
FILE: parser/parser.go
================================================
package parser
import "strings"
type ParseResult struct {
Chat string
Commands []ParseCommand
}
type ParseCommand struct {
Command string
Args string
Body string
}
func (p ParseCommand) String() string {
output := p.Command
if p.Args != "" {
output += " " + p.Args
}
if p.Body != "" {
output += "\n" + p.Body
}
return output
}
func Parse(input string) ParseResult {
tokens := Lex(input)
return ParseTokens(tokens)
}
type TokenType string
const (
Plaintext TokenType = "Plaintext"
Newline = "Newline"
Command = "Command"
Body = "Body"
)
type Token struct {
Typ TokenType
Val string
}
func ParseTokens(tokens []Token) ParseResult {
result := ParseResult{}
var activeCommand *ParseCommand
var commands []*ParseCommand
var isInCommandContext bool
for _, token := range tokens {
switch token.Typ {
case Plaintext:
if activeCommand == nil {
result.Chat += token.Val
isInCommandContext = false
continue
}
if activeCommand.Args != "" {
// ParseTokens error
panic("ParseTokens error: Command already has args")
}
activeCommand.Args = strings.TrimSpace(token.Val)
case Newline:
if activeCommand != nil {
activeCommand = nil
continue
}
if strings.HasSuffix(result.Chat, "\n\n") {
// we don't append more than two consecutive newlines
continue
}
result.Chat += token.Val
case Command:
activeCommand = &ParseCommand{Command: token.Val}
commands = append(commands, activeCommand)
isInCommandContext = true
case Body:
if activeCommand != nil {
if activeCommand.Body != "" {
// ParseTokens error
panic("ParseTokens error: Command already has Body")
}
activeCommand.Body = token.Val
continue
}
if isInCommandContext {
lastCommand := commands[len(commands)-1]
if lastCommand.Body != "" {
// ParseTokens error
panic("ParseTokens error: Command already has Body")
}
lastCommand.Body = token.Val
continue
}
result.Chat += token.Val
}
}
result.Chat = strings.TrimSpace(result.Chat)
for _, command := range commands {
result.Commands = append(result.Commands, *command)
}
return result
}
func Lex(input string) []Token {
var tokens []Token
var currentToken *Token
var nesting int
for i, c := range input {
switch c {
case '/':
// is this the start of a new Command?
if i == 0 || input[i-1] == '\n' {
if currentToken != nil {
tokens = append(tokens, *currentToken)
}
currentToken = &Token{Typ: Command, Val: "/"}
continue
}
// if we have a Token, append to it
if currentToken != nil {
currentToken.Val += string(c)
continue
}
// otherwise we can assume this is plain text
currentToken = &Token{Typ: Plaintext, Val: "/"}
case ' ':
// a space signifies the end of the Command
if currentToken != nil && currentToken.Typ == Command {
tokens = append(tokens, *currentToken)
currentToken = nil
continue
}
// if we have a Token, append to it
if currentToken != nil {
currentToken.Val += string(c)
continue
}
// otherwise we can assume this is plain text
currentToken = &Token{Typ: Plaintext, Val: " "}
case '{':
// if it's not already a Body, we'll store the current Token
if currentToken != nil && currentToken.Typ != Body {
tokens = append(tokens, *currentToken)
}
// If we already have a body, we'll add to it
if currentToken != nil && currentToken.Typ == Body {
nesting++
currentToken.Val += string(c)
continue
}
// Otherwise we'll start a new body
currentToken = &Token{Typ: Body, Val: "{"}
nesting++
case '}':
// if we're already in a Body, the Body ends
if currentToken != nil && currentToken.Typ == Body {
nesting--
currentToken.Val += "}"
if nesting == 0 {
tokens = append(tokens, *currentToken)
currentToken = nil
}
continue
}
// if we have a Token, append to it
if currentToken != nil {
currentToken.Val += string(c)
continue
}
// otherwise we can assume this is plain text
currentToken = &Token{Typ: Plaintext, Val: " "}
case '\n':
// if we have Plaintext or a Body, we'll append to it
if currentToken != nil && (currentToken.Typ == Body) {
currentToken.Val += "\n"
continue
}
// otherwise we always end the current Token at a new line
if currentToken != nil {
tokens = append(tokens, *currentToken)
currentToken = nil
}
// and we store a new line Token
tokens = append(tokens, Token{Typ: Newline, Val: "\n"})
default:
if currentToken == nil {
currentToken = &Token{Typ: Plaintext}
}
currentToken.Val += string(c)
}
}
if currentToken != nil {
tokens = append(tokens, *currentToken)
}
return tokens
}
================================================
FILE: parser/parser_test.go
================================================
package parser
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestParse(t *testing.T) {
testCases := []struct {
name string
input string
output ParseResult
}{
{
name: "basic Command",
input: "/api get /path",
output: ParseResult{
Chat: "",
Commands: []ParseCommand{
{
Command: "/api",
Args: "get /path",
Body: "",
},
},
},
},
{
name: "Command with args",
input: "/api get /path { something }",
output: ParseResult{
Chat: "",
Commands: []ParseCommand{
{
Command: "/api",
Args: "get /path",
Body: "{ something }",
},
},
},
},
{
name: "multiline Command with args",
input: `/api get /path {
something
}`,
output: ParseResult{
Chat: "",
Commands: []ParseCommand{
{
Command: "/api",
Args: "get /path",
Body: `{
something
}`,
},
},
},
},
{
name: "multiline Command with args",
input: `/api get /path
{
something
}`,
output: ParseResult{
Chat: "",
Commands: []ParseCommand{
{
Command: "/api",
Args: "get /path",
Body: `{
something
}`,
},
},
},
},
{
name: "chat with multiline Command with args",
input: `This is some chat
/api get /path
{
something
}`,
output: ParseResult{
Chat: "This is some chat",
Commands: []ParseCommand{
{
Command: "/api",
Args: "get /path",
Body: `{
something
}`,
},
},
},
},
{
name: "chat with multiple multiline Command with args",
input: `This is some chat
/api get /path
{
something
}
This is some more chat
/api post /another-path
{
something else
}`,
output: ParseResult{
Chat: "This is some chat\n\nThis is some more chat",
Commands: []ParseCommand{
{
Command: "/api",
Args: "get /path",
Body: `{
something
}`,
},
{
Command: "/api",
Args: "post /another-path",
Body: `{
something else
}`,
},
},
},
},
{
name: "multiline Command with code Body",
input: `/plugins create my-plugin
{
package main
import "fmt"
func main() {
fmt.Println("test")
}
}`,
output: ParseResult{
Chat: "",
Commands: []ParseCommand{
{
Command: "/plugins",
Args: "create my-plugin",
Body: `{
package main
import "fmt"
func main() {
fmt.Println("test")
}
}`,
},
},
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
tokens := Lex(testCase.input)
ast := ParseTokens(tokens)
assert.Equal(t, testCase.output, ast)
})
}
}
================================================
FILE: ui/theme.go
================================================
package ui
import (
"os"
"strings"
"github.com/fatih/color"
)
var theme Theme
func init() {
if strings.ToUpper(os.Getenv("GPTCHAT_THEME")) == "DARK" {
theme = DarkTheme
} else {
theme = LightTheme
}
}
type Theme struct {
Username *color.Color
Message *color.Color
Useful *color.Color
AI *color.Color
User *color.Color
Info *color.Color
Error *color.Color
Warn *color.Color
App *color.Color
AppBold *color.Color
}
var LightTheme = Theme{
Username: color.New(color.FgRed),
Message: color.New(color.FgBlue),
Useful: color.New(color.FgWhite),
AI: color.New(color.FgGreen),
User: color.New(color.FgYellow),
Info: color.New(color.FgWhite, color.Bold),
Error: color.New(color.FgHiRed, color.Bold),
Warn: color.New(color.FgHiYellow, color.Bold),
App: color.New(color.FgWhite),
AppBold: color.New(color.FgGreen, color.Bold),
}
var DarkTheme = Theme{
Username: color.New(color.FgRed),
Message: color.New(color.FgBlue),
Useful: color.New(color.FgBlack),
AI: color.New(color.FgGreen),
User: color.New(color.FgMagenta),
Info: color.New(color.FgBlack, color.Bold),
Error: color.New(color.FgHiRed, color.Bold),
Warn: color.New(color.FgHiMagenta, color.Bold),
App: color.New(color.FgBlack),
AppBold: color.New(color.FgGreen, color.Bold),
}
================================================
FILE: ui/ui.go
================================================
package ui
import (
"bufio"
"fmt"
"os"
"strings"
)
const (
User = "USER"
AI = "AI"
System = "SYSTEM"
Tool = "TOOL"
API = "API"
Module = "MODULE"
App = "APP"
)
func Error(message string, err error) {
theme.Error.Printf("ERROR: ")
theme.Useful.Printf("%s: %v\n\n", message, err)
}
func Warn(message string) {
theme.Warn.Printf("WARNING: ")
theme.Useful.Printf("%s\n", message)
}
func Info(message string) {
theme.Warn.Printf("INFO: ")
theme.Useful.Printf("%s\n", message)
}
func Welcome(title, message string) {
theme.AppBold.Printf("%s\n\n", title)
theme.App.Printf("%s\n\n", message)
}
func PrintChatDebug(name, message string) {
theme.Useful.Printf("[DEBUG] ")
PrintChat(name, message)
}
func PrintChat(name, message string) {
switch name {
case User:
theme.User.Printf("%s:\n\n", name)
theme.Message.Printf("%s\n", indent(message))
case AI:
theme.AI.Printf("%s:\n\n", name)
theme.Useful.Printf("%s\n", indent(message))
case App:
theme.AppBold.Printf("%s:\n\n", name)
theme.Useful.Printf("%s\n", indent(message))
case System:
fallthrough
case Tool:
fallthrough
case API:
fallthrough
case Module:
fallthrough
default:
theme.Username.Printf("%s:\n\n", name)
theme.Message.Printf("%s\n", indent(message))
}
}
func PromptChatInput() string {
reader := bufio.NewReader(os.Stdin)
theme.User.Printf("USER:\n\n ")
text, _ := reader.ReadString('\n')
text = strings.TrimSpace(text)
fmt.Println()
return text
}
func PromptConfirm(prompt string) bool {
reader := bufio.NewReader(os.Stdin)
theme.AppBold.Printf("%s [Y/N]: ", prompt)
text, _ := reader.ReadString('\n')
text = strings.TrimSpace(text)
fmt.Println()
return strings.ToUpper(text) == "Y"
}
func PromptInput(prompt string) string {
reader := bufio.NewReader(os.Stdin)
theme.AppBold.Printf("%s ", prompt)
text, _ := reader.ReadString('\n')
text = strings.TrimSpace(text)
return text
}
func indent(input string) string {
lines := strings.Split(string(input), "\n")
var output string
for _, line := range lines {
output += " " + line + "\n"
}
return output
}
================================================
FILE: util/strings.go
================================================
package util
const SingleQuote = "`"
const TripleQuote = "```"
gitextract_eagp9m2d/
├── LICENSE.md
├── README.md
├── chat_loop.go
├── command.go
├── config/
│ └── config.go
├── conversation.go
├── go.mod
├── go.sum
├── main.go
├── module/
│ ├── memory/
│ │ ├── memory.go
│ │ ├── recall.go
│ │ ├── storage.go
│ │ └── store.go
│ ├── module.go
│ ├── plugin/
│ │ ├── compiled/
│ │ │ └── README.md
│ │ ├── create.go
│ │ └── source/
│ │ └── README.md
│ └── plugin.go
├── parser/
│ ├── cmd/
│ │ └── main.go
│ ├── parser.go
│ └── parser_test.go
├── ui/
│ ├── theme.go
│ └── ui.go
└── util/
└── strings.go
SYMBOL INDEX (99 symbols across 18 files)
FILE: chat_loop.go
function chatLoop (line 17) | func chatLoop(cfg config.Config) {
FILE: command.go
type slashCommandResult (line 10) | type slashCommandResult struct
type slashCommand (line 28) | type slashCommand struct
function helpCommand (line 79) | func helpCommand(string) (bool, *slashCommandResult) {
function parseSlashCommand (line 90) | func parseSlashCommand(input string) (ok bool, result *slashCommandResul...
type example (line 117) | type example struct
function exampleCommand (line 144) | func exampleCommand(args string) (bool, *slashCommandResult) {
FILE: config/config.go
type Config (line 3) | type Config struct
method OpenAIAPIModel (line 20) | func (c Config) OpenAIAPIModel() string {
method OpenAIAPIKey (line 24) | func (c Config) OpenAIAPIKey() string {
method IsSupervisedMode (line 28) | func (c Config) IsSupervisedMode() bool {
method IsDebugMode (line 32) | func (c Config) IsDebugMode() bool {
method WithOpenAIAPIKey (line 36) | func (c Config) WithOpenAIAPIKey(apiKey string) Config {
method WithSupervisedMode (line 41) | func (c Config) WithSupervisedMode(supervisedMode bool) Config {
method WithDebugMode (line 46) | func (c Config) WithDebugMode(debugMode bool) Config {
method WithOpenAIAPIModel (line 51) | func (c Config) WithOpenAIAPIModel(apiModel string) Config {
function New (line 11) | func New() Config {
FILE: conversation.go
constant openingPrompt (line 44) | openingPrompt = `Hello! Please familiarise yourself with the commands yo...
function intervalPrompt (line 48) | func intervalPrompt() string {
function appendMessage (line 56) | func appendMessage(role string, message string) {
function resetConversation (line 63) | func resetConversation() {
FILE: main.go
function init (line 20) | func init() {
function main (line 83) | func main() {
FILE: module/memory/memory.go
type memory (line 12) | type memory struct
type Module (line 17) | type Module struct
method ID (line 23) | func (m *Module) ID() string {
method Load (line 27) | func (m *Module) Load(cfg config.Config, client *openai.Client) error {
method UpdateConfig (line 33) | func (m *Module) UpdateConfig(cfg config.Config) {
method Execute (line 37) | func (m *Module) Execute(args, body string) (string, error) {
method Prompt (line 48) | func (m *Module) Prompt() string {
constant memoryPrompt (line 52) | memoryPrompt = `You also have a working long term memory.
FILE: module/memory/recall.go
method Recall (line 11) | func (m *Module) Recall(input string) (string, error) {
FILE: module/memory/storage.go
method loadFromFile (line 9) | func (m *Module) loadFromFile() error {
method writeToFile (line 28) | func (m *Module) writeToFile() error {
method appendMemory (line 41) | func (m *Module) appendMemory(mem memory) error {
FILE: module/memory/store.go
method Store (line 8) | func (m *Module) Store(input string) (string, error) {
FILE: module/module.go
type Module (line 12) | type Module interface
type IntervalPrompt (line 21) | type IntervalPrompt interface
function Load (line 27) | func Load(cfg config.Config, client *openai.Client, modules ...Module) e...
function UpdateConfig (line 41) | func UpdateConfig(cfg config.Config) {
function IsLoaded (line 53) | func IsLoaded(id string) bool {
function LoadPlugin (line 58) | func LoadPlugin(m Module) error {
type CommandResult (line 66) | type CommandResult struct
function HelpCommand (line 71) | func HelpCommand() (bool, *CommandResult) {
function ExecuteCommand (line 86) | func ExecuteCommand(command, args, body string) (bool, *CommandResult) {
FILE: module/plugin.go
type Plugin (line 15) | type Plugin interface
type pluginLoader (line 21) | type pluginLoader struct
method Load (line 25) | func (p pluginLoader) Load(config.Config, *openai.Client) error {
method UpdateConfig (line 28) | func (p pluginLoader) UpdateConfig(config.Config) {}
method ID (line 29) | func (p pluginLoader) ID() string {
method Prompt (line 32) | func (p pluginLoader) Prompt() string {
method Execute (line 35) | func (p pluginLoader) Execute(args, body string) (string, error) {
function GetModuleForPlugin (line 57) | func GetModuleForPlugin(p Plugin) Module {
function LoadCompiledPlugins (line 61) | func LoadCompiledPlugins() error {
function OpenPlugin (line 95) | func OpenPlugin(path string) (Plugin, error) {
FILE: module/plugin/create.go
function CheckPaths (line 27) | func CheckPaths() error {
type Module (line 47) | type Module struct
method Load (line 52) | func (m *Module) Load(cfg config.Config, client *openai.Client) error {
method UpdateConfig (line 63) | func (m *Module) UpdateConfig(cfg config.Config) {
method Prompt (line 67) | func (m *Module) Prompt() string {
method ID (line 71) | func (m *Module) ID() string {
method Execute (line 75) | func (m *Module) Execute(args, body string) (string, error) {
method createPlugin (line 90) | func (m *Module) createPlugin(id, body string) (string, error) {
FILE: parser/cmd/main.go
function main (line 9) | func main() {
function indent (line 43) | func indent(input string, prefix string) string {
FILE: parser/parser.go
type ParseResult (line 5) | type ParseResult struct
type ParseCommand (line 10) | type ParseCommand struct
method String (line 16) | func (p ParseCommand) String() string {
function Parse (line 27) | func Parse(input string) ParseResult {
type TokenType (line 32) | type TokenType
constant Plaintext (line 35) | Plaintext TokenType = "Plaintext"
constant Newline (line 36) | Newline = "Newline"
constant Command (line 37) | Command = "Command"
constant Body (line 38) | Body = "Body"
type Token (line 41) | type Token struct
function ParseTokens (line 46) | func ParseTokens(tokens []Token) ParseResult {
function Lex (line 109) | func Lex(input string) []Token {
FILE: parser/parser_test.go
function TestParse (line 8) | func TestParse(t *testing.T) {
FILE: ui/theme.go
function init (line 12) | func init() {
type Theme (line 20) | type Theme struct
FILE: ui/ui.go
constant User (line 11) | User = "USER"
constant AI (line 12) | AI = "AI"
constant System (line 13) | System = "SYSTEM"
constant Tool (line 14) | Tool = "TOOL"
constant API (line 15) | API = "API"
constant Module (line 16) | Module = "MODULE"
constant App (line 17) | App = "APP"
function Error (line 20) | func Error(message string, err error) {
function Warn (line 25) | func Warn(message string) {
function Info (line 30) | func Info(message string) {
function Welcome (line 35) | func Welcome(title, message string) {
function PrintChatDebug (line 40) | func PrintChatDebug(name, message string) {
function PrintChat (line 45) | func PrintChat(name, message string) {
function PromptChatInput (line 70) | func PromptChatInput() string {
function PromptConfirm (line 80) | func PromptConfirm(prompt string) bool {
function PromptInput (line 90) | func PromptInput(prompt string) string {
function indent (line 98) | func indent(input string) string {
FILE: util/strings.go
constant SingleQuote (line 3) | SingleQuote = "`"
constant TripleQuote (line 4) | TripleQuote = "```"
Condensed preview — 24 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (63K chars).
[
{
"path": "LICENSE.md",
"chars": 1074,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2023 Ian Kent\n\nPermission is hereby granted, free of charge, to any person obtainin"
},
{
"path": "README.md",
"chars": 2875,
"preview": "GPTChat\n=======\n\nGPTChat is a client which gives GPT-4 some unique tools to be a better AI.\n\nWith GPTChat, GPT-4 can:\n* "
},
{
"path": "chat_loop.go",
"chars": 4581,
"preview": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/ian-kent/gptchat/config\"\n\t\"github.com/ian-kent"
},
{
"path": "command.go",
"chars": 3259,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"github.com/ian-kent/gptchat/ui\"\n\t\"os\"\n\t\"strings\"\n)\n\ntype slashCommandResult struct {\n\t//"
},
{
"path": "config/config.go",
"chars": 933,
"preview": "package config\n\ntype Config struct {\n\topenaiAPIKey string\n\topenaiAPIModel string\n\n\tsupervisedMode bool\n\tdebugMode "
},
{
"path": "conversation.go",
"chars": 1813,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"github.com/ian-kent/gptchat/util\"\n\t\"github.com/sashabaranov/go-openai\"\n\t\"time\"\n)\n\nvar sy"
},
{
"path": "go.mod",
"chars": 911,
"preview": "module github.com/ian-kent/gptchat\n\ngo 1.18\n\nrequire (\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/dlclar"
},
{
"path": "go.sum",
"chars": 8627,
"preview": "github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=\ngithub.com/chzyer/readline v1.5.0/"
},
{
"path": "main.go",
"chars": 2237,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/ian-kent/gptchat/config\"\n\t\"github.com/ian-kent/g"
},
{
"path": "module/memory/memory.go",
"chars": 2965,
"preview": "package memory\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/ian-kent/gptchat/config\"\n\t\"github.com/ian-kent/gptchat/util\"\n\top"
},
{
"path": "module/memory/recall.go",
"chars": 1846,
"preview": "package memory\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\n\t\"github.com/ian-kent/gptchat/util\"\n\t\"github.com/sashabaranov/go-o"
},
{
"path": "module/memory/storage.go",
"chars": 668,
"preview": "package memory\n\nimport (\n\t\"encoding/json\"\n\t\"io/ioutil\"\n\t\"os\"\n)\n\nfunc (m *Module) loadFromFile() error {\n\t_, err := os.St"
},
{
"path": "module/memory/store.go",
"chars": 401,
"preview": "package memory\n\nimport (\n\t\"github.com/ian-kent/gptchat/util\"\n\t\"time\"\n)\n\nfunc (m *Module) Store(input string) (string, er"
},
{
"path": "module/module.go",
"chars": 2593,
"preview": "package module\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/ian-kent/gptchat/config\"\n\t\"github.com/ian-kent/gptchat/ui\"\n\topena"
},
{
"path": "module/plugin/compiled/README.md",
"chars": 49,
"preview": "Compiled plugins will be added to this directory."
},
{
"path": "module/plugin/create.go",
"chars": 7262,
"preview": "package plugin\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/fatih/color\"\n\t\"github.com/ian-kent/gptchat/config\"\n\t\"github.com/i"
},
{
"path": "module/plugin/source/README.md",
"chars": 46,
"preview": "Plugin source will be added to this directory."
},
{
"path": "module/plugin.go",
"chars": 2480,
"preview": "package module\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/ian-kent/gptchat/config\"\n\t\"github.com/ian-kent/g"
},
{
"path": "parser/cmd/main.go",
"chars": 976,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"github.com/ian-kent/gptchat/parser\"\n\t\"strings\"\n)\n\nfunc main() {\n\tinput := `/plugins crea"
},
{
"path": "parser/parser.go",
"chars": 4822,
"preview": "package parser\n\nimport \"strings\"\n\ntype ParseResult struct {\n\tChat string\n\tCommands []ParseCommand\n}\n\ntype ParseComma"
},
{
"path": "parser/parser_test.go",
"chars": 2677,
"preview": "package parser\n\nimport (\n\t\"github.com/stretchr/testify/assert\"\n\t\"testing\"\n)\n\nfunc TestParse(t *testing.T) {\n\ttestCases :"
},
{
"path": "ui/theme.go",
"chars": 1361,
"preview": "package ui\n\nimport (\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/fatih/color\"\n)\n\nvar theme Theme\n\nfunc init() {\n\tif strings.ToUpper(o"
},
{
"path": "ui/ui.go",
"chars": 2117,
"preview": "package ui\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n)\n\nconst (\n\tUser = \"USER\"\n\tAI = \"AI\"\n\tSystem = \"SYSTEM\"\n\tToo"
},
{
"path": "util/strings.go",
"chars": 64,
"preview": "package util\n\nconst SingleQuote = \"`\"\nconst TripleQuote = \"```\"\n"
}
]
About this extraction
This page contains the full source code of the ian-kent/gptchat GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 24 files (55.3 KB), approximately 18.0k tokens, and a symbol index with 99 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.