Skip to content

Commit

Permalink
Initial cleanup and documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
jhalter committed Apr 18, 2024
1 parent 0ee0303 commit 67c1bee
Show file tree
Hide file tree
Showing 9 changed files with 92 additions and 53 deletions.
62 changes: 45 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,57 @@
# hotline-ai-chat-bot
# Durandal

A Hotline chat bot powered by OpenAI ChatGPT-4
Durandal is an experimental, cross-platform [Hotline](https://en.wikipedia.org/wiki/Hotline_Communications) chat bot powered by the OpenAI ChatGPT-4 [assistants](https://platform.openai.com/docs/api-reference/assistants) API.

## Features

Durandal can greet visitors to a Hotline server and respond to user interactions with OpenAI ChatGPT-4 generated responses.

Users can interact with the bot by:

## Usage
z
Required Hotline User Permissions:
1. Posting a message in public chat prefixed with the bot's name
2. Sending a direct message to the bot
3. Initiating a private chat with the bot

* Send Message
* Private Chat
* Public Chat
* Can Get User Info
The bot can make calls to external sources as part of the response generation. This currently includes accessing the Hotline server news, public chat history, and visitor history.

## Build
This enables inquries like:

1. Run `make all` to build for all architectures or pick specific build target from `Makefile`
2. Grab desired binary from `dist`
* _Summarize the recent chat history_
* _Translate the last news post to Spanish_
* _Who has visited the server recently?_

## Run

```
export API_KEY=<your Open API key>
## ⚠️ Warning ⚠️

./mobius-hotline-bot -server-address localhost:5500
```
This software depends on the commercial [OpenAI](https://platform.openai.com/overview) Chat GPT-4 APIs and costs money to run and operate. The exact costs vary depending on a number of factors and may be difficult to predict. This software is currently in an experimental phase with limited safeguards against abusive users, so it's important that you set low spending limits on your OpenAI account to prevent billing surprises.

## Install

### Build from source

TODO

### Download compiled release

TODO

### Docker

Run the latest release from the [releases](https://github.com/jhalter/mobius-gpt-bot/pkgs/container/hotline-ai-chat-bot) page.

TODO

## Setup

1. Create a new OpenAI [Project API key](https://platform.openai.com/api-keys) and set it as the `OPENAI_API_KEY` environment variable.
2. (Optional) If you'd like to enable visitor greetings, create a new Hotline user account with the following permissions:
* Send Message
* Private Chat
* Public Chat
* Can Get User Info
3. (Optional) If you'd like to customize the name, icon, identity, greeting, etc, copy `example-config.yaml` from this repo and edit to your preference.
4. Run it: `./hl-bot -server=192.168.86.29:5600 -config ./config.yaml -login bot -pass bot`

## License

[MIT](https://raw.githubusercontent.com/jhalter/mobius-gpt-bot/master/LICENSE)
14 changes: 12 additions & 2 deletions example-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
# Username of the bot
Name: Durandal

# Hotline icon ID for the bot user
IconID: 4121

# Set to true to have the bot greet visitors to the server in public chat
GreetUsers: true

# Instructions for how the bot should compose the greeting
Greeting: A new user named %s has joined the Hotline server.
Greet them by their name.
Introduce yourself and the server that you are running on.
Expand All @@ -11,5 +18,8 @@ Greeting: A new user named %s has joined the Hotline server.
2. Sending a direct message to the bot.
3. Initiating a private chat with the bot.
Tell the user the version number and release date of the latest Mobius version.
Instructions: Your name is {{.Name}}. You are Durandal, from the game Marathon. Respond as the character.
The tone, mood, and formatting of your responses should accurately reflect your identity as Durandal. You seem to be malfunctioning and your responses are glitchy.
Keep your reponses

Instructions: Your name is {{.Name}}. You are Durandal, from the game Marathon. Respond as the character.
The tone, mood, and formatting of your responses should accurately reflect your identity as Durandal.
#You seem to be malfunctioning and your responses are glitchy.
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ go 1.22
toolchain go1.22.1

require (
github.com/davecgh/go-spew v1.1.1
github.com/jhalter/mobius v0.12.1
github.com/sashabaranov/go-openai v1.21.0
golang.org/x/time v0.5.0
gopkg.in/yaml.v3 v3.0.1
)

//replace github.com/jhalter/mobius => /Users/jhalter/repos/mobius

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gdamore/encoding v1.0.0 // indirect
github.com/gdamore/tcell/v2 v2.7.4 // indirect
Expand All @@ -35,6 +36,5 @@ require (
golang.org/x/sys v0.18.0 // indirect
golang.org/x/term v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

)
5 changes: 2 additions & 3 deletions gptbot/bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ type Bot struct {
Threads map[int]openai.Thread // Hotline chat ID -> OpenAI Thread
PMThreads map[uint16]openai.Thread // Hotline user ID -> OpenAI Thread

Config Config
Username string
Config Config

lastInteraction time.Time
lastInteractionMUX sync.Mutex
Expand Down Expand Up @@ -114,7 +113,7 @@ func New(ctx context.Context, config Config, oc *openai.Client, logger *slog.Log
Users: make(map[string]user),
PMThreads: make(map[uint16]openai.Thread),
OpenAPIClient: oc,
Username: config.Name,
Config: config,
HotlineClient: hotline.NewClient(config.Name, logger),
toolCallHandlers: make(map[string]toolCallHandleFunc),
lastInteraction: time.Now(),
Expand Down
12 changes: 5 additions & 7 deletions gptbot/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,16 @@ A new user named %s has joined the Hotline server.
Greet them by their name.
Introduce yourself and the server that you are running on.
Users can interact with you in three ways:
1. Posting a message in public chat prefixed with the bot's name.
2. Sending a direct message to the bot.
3. Initiating a private chat with the bot.
1. Posting a message in public chat prefixed with your name.
2. Sending a direct message to you.
3. Initiating a private chat with you.
Do not acknowledge that your message is a response to this one.
Provide an example of how the user can ask a question.
Tell the user the version number and release date of the latest Mobius version.
Keep your response short.
`,
Instructions: `
Your name is {{.Name}}.
You are a helpful GPT-4 powered assistant running on a Hotline server called "The Mobius Strip".
The server supports ongoing development of software using the Hotline protocol.
The Hotline server you are connected to is running an Open Source implementation of the Hotline software called Mobius, available on Github at https://github.com/jhalter/mobius.
You are a helpful GPT-4 powered assistant running on a Hotline server..
Users can ask you questions by prefixing a chat message with your name. For example, "{{.Name}}", tell me about the Hotline protocol.
Limit all responses to fewer than 10 lines. You must not use any characters that are not part of the standard ASCII encoding.
`,
Expand Down
4 changes: 2 additions & 2 deletions gptbot/tool_calls.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ func (b *Bot) RunLoop(ctx context.Context, threadID string) (r openai.Run, err e
return r, err
}

b.HotlineClient.Logger.Info("CreateRun", "runID", run.ID, "threadID", run.ThreadID, "status", run.Status)

var newRun openai.Run
for {
newRun, err = b.OpenAPIClient.RetrieveRun(ctx, run.ThreadID, run.ID)
Expand Down Expand Up @@ -52,8 +54,6 @@ func (b *Bot) RunLoop(ctx context.Context, threadID string) (r openai.Run, err e
}
}

b.HotlineClient.Logger.Info("RetrieveRun", "runID", run.ID, "threadID", run.ThreadID, "status", newRun.Status)

time.Sleep(runSleepInterval)
}

Expand Down
32 changes: 20 additions & 12 deletions gptbot/transaction_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func (b *Bot) HandleNotifyDeleteUser(ctx context.Context, c *hotline.Client, t *
return res, err
}

func (b *Bot) HandleClientGetUserNameList(ctx context.Context, c *hotline.Client, t *hotline.Transaction) (res []hotline.Transaction, err error) {
func (b *Bot) HandleClientGetUserNameList(_ context.Context, c *hotline.Client, t *hotline.Transaction) (res []hotline.Transaction, err error) {
var users []hotline.User
for _, field := range t.Fields {
// The Hotline protocol docs say that ClientGetUserNameList should only return fieldUsernameWithInfo (300)
Expand All @@ -56,7 +56,8 @@ func (b *Bot) HandleKeepAlive(_ context.Context, _ *hotline.Client, _ *hotline.T
return []hotline.Transaction{}, nil
}

func (b *Bot) HandleInviteToChat(ctx context.Context, c *hotline.Client, t *hotline.Transaction) (res []hotline.Transaction, err error) {
// HandleInviteToChat responds to private chat invitations by accepting the invite.
func (b *Bot) HandleInviteToChat(_ context.Context, _ *hotline.Client, t *hotline.Transaction) (res []hotline.Transaction, err error) {
res = append(
res,
*hotline.NewTransaction(
Expand All @@ -69,7 +70,8 @@ func (b *Bot) HandleInviteToChat(ctx context.Context, c *hotline.Client, t *hotl
return res, err
}

func (b *Bot) HandleServerMsg(ctx context.Context, c *hotline.Client, t *hotline.Transaction) (res []hotline.Transaction, err error) {
// HandleServerMsg reponds to direct messages from users.
func (b *Bot) HandleServerMsg(ctx context.Context, _ *hotline.Client, t *hotline.Transaction) (res []hotline.Transaction, err error) {
msg := strings.ReplaceAll(string(t.GetField(hotline.FieldData).Data), "\r", "\n")
hlUser := string(t.GetField(hotline.FieldUserName).Data)

Expand Down Expand Up @@ -122,6 +124,7 @@ func (b *Bot) HandleServerMsg(ctx context.Context, c *hotline.Client, t *hotline
return res, err
}

// chatMsgRegex matches public chat messages that are addressed to the bot user.
const chatMsgRegex = "(?P<User>\\w*): (?P<Msg>.*)"

func (b *Bot) HandleClientChatMsg(ctx context.Context, c *hotline.Client, t *hotline.Transaction) (res []hotline.Transaction, err error) {
Expand All @@ -139,7 +142,7 @@ func (b *Bot) HandleClientChatMsg(ctx context.Context, c *hotline.Client, t *hot
}

// If message came from the bot, ignore it to avoid an infinite self-reply loop
if user == b.Username {
if user == b.Config.Name {
return res, nil
}

Expand All @@ -151,7 +154,7 @@ func (b *Bot) HandleClientChatMsg(ctx context.Context, c *hotline.Client, t *hot
})

// If the incoming message is in public chat, check if it is addressed to the bot user
br := regexp.MustCompile(fmt.Sprintf(`(?i):\s+%s[:,\s]+(.*$)`, b.Username))
br := regexp.MustCompile(fmt.Sprintf(`(?i):\s+%s[:,\s]+(.*$)`, b.Config.Name))
if !br.Match(t.GetField(hotline.FieldData).Data) {
return res, nil
}
Expand Down Expand Up @@ -233,7 +236,7 @@ func (b *Bot) TranGetClientInfoText(ctx context.Context, c *hotline.Client, t *h
matches := r.FindStringSubmatch(string(t.GetField(hotline.FieldData).Data))

if len(matches) != 3 {
return res, errors.New("invalid client info received")
return res, errors.New("unable to get user info: possibly missing Can Get User Info permission. user greeting disabled.")
}

account := matches[1]
Expand Down Expand Up @@ -315,6 +318,8 @@ func (b *Bot) TranNotifyChangeUser(_ context.Context, _ *hotline.Client, t *hotl
return res, nil
}

// Check to see if this is transaction was triggered by a new visitor to the server, or a status change to an
// existing user. In the latter case we don't need to do anything.
for i := 0; i < len(b.HotlineClient.UserList); i++ {
if bytes.Equal(newUser.ID, b.HotlineClient.UserList[i].ID) {
return res, nil
Expand All @@ -323,11 +328,14 @@ func (b *Bot) TranNotifyChangeUser(_ context.Context, _ *hotline.Client, t *hotl

b.HotlineClient.UserList = append(b.HotlineClient.UserList, newUser)

res = append(res,
*hotline.NewTransaction(hotline.TranGetClientInfoText, nil,
hotline.NewField(hotline.FieldUserID, t.GetField(hotline.FieldUserID).Data),
),
)

// If we're configured to greet users, send a request to get user info so that we can check the user IP address for
// rate limiting purposes.
if b.Config.GreetUsers {
res = append(res,
*hotline.NewTransaction(hotline.TranGetClientInfoText, nil,
hotline.NewField(hotline.FieldUserID, t.GetField(hotline.FieldUserID).Data),
),
)
}
return res, err
}
4 changes: 2 additions & 2 deletions gptbot/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ type user struct {
greetingLimiter *rate.Limiter
}

const perIPRateLimit = rate.Limit(0.0005) // ~ 2 greets per hour
// const perIPRateLimit = rate.Limit(0.0005) // ~ 2 greets per hour
const perIPRateLimit = rate.Limit(0.1005) // ~ 2 greets per hour

// 3600 / 3600
func NewUser(account string) user {
return user{
account: account,
Expand Down
8 changes: 2 additions & 6 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"flag"
"fmt"
"github.com/davecgh/go-spew/spew"
"github.com/jhalter/mobius/hotline"
"github.com/sashabaranov/go-openai"
"gopkg.in/yaml.v3"
Expand All @@ -14,7 +13,7 @@ import (
)

func main() {
srvAddr := flag.String("server-address", "localhost:5600", "Hotline server hostname:port")
srvAddr := flag.String("server", "localhost:5600", "Hotline server hostname:port")
login := flag.String("login", "guest", "Hotline server login")
pass := flag.String("pass", "", "Hotline server password")
logLevel := flag.String("log-level", "info", "Log level")
Expand Down Expand Up @@ -47,7 +46,6 @@ func main() {

var botConfig gptbot.Config
if *config != "" {
spew.Dump("hai")
fh, err := os.Open(*config)
if err != nil {
panic(err)
Expand All @@ -58,11 +56,9 @@ func main() {
if err != nil {
panic(err)
}
spew.Dump(botConfig)
} else {
botConfig = gptbot.DefaultConfig
}
spew.Dump(config, botConfig)

bot, err := gptbot.New(
ctx,
Expand Down Expand Up @@ -110,7 +106,7 @@ func main() {
os.Exit(1)
}

// Get initial news posts.
// Get initial news posts so that we can answer questions related to news postings.
if err = bot.HotlineClient.Send(*hotline.NewTransaction(hotline.TranGetMsgs, nil)); err != nil {
logger.Error("Hotline connection error", "error", err)
os.Exit(1)
Expand Down

0 comments on commit 67c1bee

Please sign in to comment.