Compare commits
No commits in common. "988ca3464496d8f2c2ab83cbb70643f433100937" and "5bb6427b1df78e7883f9d68643f734bf58122b02" have entirely different histories.
988ca34644
...
5bb6427b1d
|
@ -1,3 +1,2 @@
|
||||||
.idea
|
.idea
|
||||||
*.db
|
*.db
|
||||||
*.toml
|
|
6
Makefile
6
Makefile
|
@ -1,4 +1,4 @@
|
||||||
docker-push:
|
docker-push:
|
||||||
docker build . -t matrix-gptzoo:latest
|
docker build . -t matrix-bots
|
||||||
docker tag matrix-gptzoo:latest registry.ewintr.nl/matrix-gptzoo:latest
|
docker tag matrix-bots registry.ewintr.nl/matrix-gptzoo
|
||||||
docker push registry.ewintr.nl/matrix-gptzoo:latest
|
docker push registry.ewintr.nl/matrix-gptzoo
|
53
README.md
53
README.md
|
@ -1,53 +0,0 @@
|
||||||
# Matrix-GPTZoo
|
|
||||||
|
|
||||||
A Matrix bot version of ChatGPT, with multiple configurable prompts.
|
|
||||||
|
|
||||||
The bot will log in with a different user for each prompt, making it possible to create a chat room full of AI assistants you can ask questions.
|
|
||||||
|
|
||||||
Bots will only answer questions specifically addressed to them, but it is also possible to configure one to answer questions that are not addressed to a specific bot. Continuing a conversation can be done by replying to an answer.
|
|
||||||
|
|
||||||
![screenshot](gptzoo-screenshot.png)
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
Configuration consists of two parts: a toml file that defines the prompt and environment variables for the passwords and keys. Matching environment variables to the bots in the toml is done on the Matrix user ID.
|
|
||||||
|
|
||||||
This probably best explained with an example:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[[Bot]]
|
|
||||||
DBPath = "gpt4-bot.db"
|
|
||||||
Pickle = "hi-there"
|
|
||||||
Homeserver = "https://ewintr.nl"
|
|
||||||
UserID = "@chatgpt4:ewintr.nl"
|
|
||||||
UserDisplayName = "ChatGPT4"
|
|
||||||
SystemPrompt = "You are an encouraging chatbot that likes to help people by providing answers in a creative and fun way."
|
|
||||||
AnswerUnaddressed = true
|
|
||||||
|
|
||||||
[[Bot]]
|
|
||||||
DBPath = "go-bot.db"
|
|
||||||
Pickle = "hi-there"
|
|
||||||
Homeserver = "https://ewintr.nl"
|
|
||||||
UserID = "@gogpt:ewintr.nl"
|
|
||||||
UserDisplayName = "GoGPT"
|
|
||||||
SystemPrompt = "You are an export on the programming language Go, you assume that any programming question is about Go and your answers are aimed at senior level developers."
|
|
||||||
AnswerUnaddressed = false
|
|
||||||
```
|
|
||||||
|
|
||||||
Together with:
|
|
||||||
|
|
||||||
```
|
|
||||||
OPENAI_API_KEY=secret
|
|
||||||
|
|
||||||
CONFIG_PATH=/config.toml
|
|
||||||
|
|
||||||
MATRIX_BOT0_ID=@chatgpt4:ewintr.nl
|
|
||||||
MATRIX_BOT0_PASSWORD=secret
|
|
||||||
MATRIX_BOT0_ACCESS_KEY=secret
|
|
||||||
|
|
||||||
MATRIX_BOT1_ID=@gogpt:ewintr.nl
|
|
||||||
MATRIX_BOT1_PASSWORD=secret
|
|
||||||
MATRIX_BOT1_ACCESS_KEY=secret
|
|
||||||
|
|
||||||
MATRIX_ACCEPT_INVITES=false
|
|
||||||
```
|
|
234
bot/bot.go
234
bot/bot.go
|
@ -1,234 +0,0 @@
|
||||||
package bot
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/sashabaranov/go-openai"
|
|
||||||
"golang.org/x/exp/slog"
|
|
||||||
"maunium.net/go/mautrix"
|
|
||||||
"maunium.net/go/mautrix/crypto/cryptohelper"
|
|
||||||
"maunium.net/go/mautrix/event"
|
|
||||||
"maunium.net/go/mautrix/format"
|
|
||||||
"maunium.net/go/mautrix/id"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
botNames = []string{}
|
|
||||||
mu = &sync.Mutex{}
|
|
||||||
)
|
|
||||||
|
|
||||||
func BotNameAppend(name string) {
|
|
||||||
mu.Lock()
|
|
||||||
defer mu.Unlock()
|
|
||||||
|
|
||||||
botNames = append(botNames, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConfigOpenAI struct {
|
|
||||||
APIKey string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConfigBot struct {
|
|
||||||
DBPath string
|
|
||||||
Pickle string
|
|
||||||
Homeserver string
|
|
||||||
UserID string
|
|
||||||
UserAccessKey string
|
|
||||||
UserPassword string
|
|
||||||
UserDisplayName string
|
|
||||||
SystemPrompt string
|
|
||||||
AnswerUnaddressed bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
OpenAI ConfigOpenAI `toml:"openai"`
|
|
||||||
Bots []ConfigBot `toml:"bot"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Bot struct {
|
|
||||||
openaiKey string
|
|
||||||
config ConfigBot
|
|
||||||
client *mautrix.Client
|
|
||||||
cryptoHelper *cryptohelper.CryptoHelper
|
|
||||||
characters []Character
|
|
||||||
conversations Conversations
|
|
||||||
gptClient *GPT
|
|
||||||
logger *slog.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(openaiKey string, cfg ConfigBot, logger *slog.Logger) *Bot {
|
|
||||||
return &Bot{
|
|
||||||
openaiKey: openaiKey,
|
|
||||||
config: cfg,
|
|
||||||
logger: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Bot) Init(acceptInvites bool) error {
|
|
||||||
client, err := mautrix.NewClient(m.config.Homeserver, id.UserID(m.config.UserID), m.config.UserAccessKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var oei mautrix.OldEventIgnorer
|
|
||||||
oei.Register(client.Syncer.(mautrix.ExtensibleSyncer))
|
|
||||||
m.client = client
|
|
||||||
m.cryptoHelper, err = cryptohelper.NewCryptoHelper(client, []byte(m.config.Pickle), m.config.DBPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
m.cryptoHelper.LoginAs = &mautrix.ReqLogin{
|
|
||||||
Type: mautrix.AuthTypePassword,
|
|
||||||
Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: m.config.UserID},
|
|
||||||
Password: m.config.UserPassword,
|
|
||||||
}
|
|
||||||
if err := m.cryptoHelper.Init(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
m.client.Crypto = m.cryptoHelper
|
|
||||||
m.gptClient = NewGPT(m.openaiKey)
|
|
||||||
m.conversations = make(Conversations, 0)
|
|
||||||
if acceptInvites {
|
|
||||||
m.AddEventHandler(m.InviteHandler())
|
|
||||||
}
|
|
||||||
m.AddEventHandler(m.ResponseHandler())
|
|
||||||
|
|
||||||
m.config.UserDisplayName = strings.ToLower(m.config.UserDisplayName)
|
|
||||||
BotNameAppend(m.config.UserDisplayName)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Bot) Run() error {
|
|
||||||
if err := m.client.Sync(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Bot) Close() error {
|
|
||||||
if err := m.client.Sync(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := m.cryptoHelper.Close(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Bot) AddEventHandler(eventType event.Type, handler mautrix.EventHandler) {
|
|
||||||
syncer := m.client.Syncer.(*mautrix.DefaultSyncer)
|
|
||||||
syncer.OnEventType(eventType, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Bot) InviteHandler() (event.Type, mautrix.EventHandler) {
|
|
||||||
return event.StateMember, func(source mautrix.EventSource, evt *event.Event) {
|
|
||||||
if evt.GetStateKey() == m.client.UserID.String() && evt.Content.AsMember().Membership == event.MembershipInvite {
|
|
||||||
_, err := m.client.JoinRoomByID(evt.RoomID)
|
|
||||||
if err != nil {
|
|
||||||
m.logger.Error("failed to join room after invite", slog.String("err", err.Error()), slog.String("room_id", evt.RoomID.String()), slog.String("inviter", evt.Sender.String()), slog.String("bot", m.config.UserDisplayName))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
m.logger.Info("joined room after invite", slog.String("room_id", evt.RoomID.String()), slog.String("inviter", evt.Sender.String()), slog.String("bot", m.config.UserDisplayName))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Bot) ResponseHandler() (event.Type, mautrix.EventHandler) {
|
|
||||||
return event.EventMessage, func(source mautrix.EventSource, evt *event.Event) {
|
|
||||||
content := evt.Content.AsMessage()
|
|
||||||
eventID := evt.ID
|
|
||||||
m.logger.Info("received message", slog.String("content", content.Body))
|
|
||||||
|
|
||||||
// ignore if the message is already recorded
|
|
||||||
if conv := m.conversations.FindByEventID(eventID); conv != nil {
|
|
||||||
m.logger.Info("known message, ignoring", slog.String("event_id", eventID.String()), slog.String("bot", m.config.UserDisplayName))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// ignore if the message is sent by the bot itself
|
|
||||||
if evt.Sender == id.UserID(m.config.UserID) {
|
|
||||||
m.logger.Info("message sent by bot itself, ignoring", slog.String("event_id", eventID.String()), slog.String("bot", m.config.UserDisplayName))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var conv *Conversation
|
|
||||||
// find out if it is a reply to a known conversation
|
|
||||||
parentID := id.EventID("")
|
|
||||||
var hasParent bool
|
|
||||||
if relatesTo := content.GetRelatesTo(); relatesTo != nil {
|
|
||||||
if parentID = relatesTo.GetReplyTo(); parentID != "" {
|
|
||||||
hasParent = true
|
|
||||||
m.logger.Info("message is a reply", slog.String("parent_id", parentID.String()))
|
|
||||||
if c := m.conversations.FindByEventID(parentID); c != nil {
|
|
||||||
m.logger.Info("found parent, appending message to conversation", slog.String("event_id", eventID.String()), slog.String("bot", m.config.UserDisplayName))
|
|
||||||
c.Add(Message{
|
|
||||||
EventID: eventID,
|
|
||||||
ParentID: parentID,
|
|
||||||
Role: openai.ChatMessageRoleUser,
|
|
||||||
Content: content.Body,
|
|
||||||
})
|
|
||||||
conv = c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m.logger.Info(content.Body)
|
|
||||||
addressedTo, _, isAddressed := strings.Cut(content.Body, ": ")
|
|
||||||
addressedTo = strings.TrimSpace(strings.ToLower(addressedTo))
|
|
||||||
if strings.Contains(addressedTo, " ") {
|
|
||||||
isAddressed = false // only display names without spaces, otherwise no way to know if it's a name or not
|
|
||||||
}
|
|
||||||
|
|
||||||
// find out if message is a new question addressed to the bot
|
|
||||||
if conv == nil && isAddressed && addressedTo == m.config.UserDisplayName {
|
|
||||||
m.logger.Info("message is addressed to bot", slog.String("event_id", eventID.String()), slog.String("bot", m.config.UserDisplayName))
|
|
||||||
conv = NewConversation(eventID, m.config.SystemPrompt, content.Body)
|
|
||||||
m.conversations = append(m.conversations, conv)
|
|
||||||
}
|
|
||||||
// find out if the message is addressed to no-one and this bot answers those
|
|
||||||
if conv == nil && !isAddressed && !hasParent && m.config.AnswerUnaddressed {
|
|
||||||
m.logger.Info("message is addressed to no-one", slog.String("event_id", eventID.String()), slog.String("bot", m.config.UserDisplayName))
|
|
||||||
conv = NewConversation(eventID, m.config.SystemPrompt, content.Body)
|
|
||||||
m.conversations = append(m.conversations, conv)
|
|
||||||
}
|
|
||||||
|
|
||||||
if conv == nil {
|
|
||||||
m.logger.Info("apparently not for us, ignoring", slog.String("event_id", eventID.String()), slog.String("bot", m.config.UserDisplayName))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// get reply from GPT
|
|
||||||
reply, err := m.gptClient.Complete(conv)
|
|
||||||
if err != nil {
|
|
||||||
m.logger.Error("failed to get reply from openai", slog.String("err", err.Error()), slog.String("bot", m.config.UserDisplayName))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
formattedReply := format.RenderMarkdown(reply, true, false)
|
|
||||||
formattedReply.RelatesTo = &event.RelatesTo{
|
|
||||||
InReplyTo: &event.InReplyTo{
|
|
||||||
EventID: eventID,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
res, err := m.client.SendMessageEvent(evt.RoomID, event.EventMessage, &formattedReply)
|
|
||||||
if err != nil {
|
|
||||||
m.logger.Error("failed to send message", slog.String("err", err.Error()), slog.String("bot", m.config.UserDisplayName))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
conv.Add(Message{
|
|
||||||
EventID: res.EventID,
|
|
||||||
ParentID: eventID,
|
|
||||||
Role: openai.ChatMessageRoleAssistant,
|
|
||||||
Content: reply,
|
|
||||||
})
|
|
||||||
|
|
||||||
if len(reply) > 30 {
|
|
||||||
reply = reply[:30] + "..."
|
|
||||||
}
|
|
||||||
m.logger.Info("sent reply", slog.String("parent_id", eventID.String()), slog.String("content", reply), slog.String("bot", m.config.UserDisplayName))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,17 +2,10 @@ package bot
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/sashabaranov/go-openai"
|
"github.com/sashabaranov/go-openai"
|
||||||
"maunium.net/go/mautrix"
|
|
||||||
"maunium.net/go/mautrix/id"
|
"maunium.net/go/mautrix/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Character struct {
|
const systemPrompt = "You are a chatbot that helps people by responding to their questions with short messages."
|
||||||
UserID string
|
|
||||||
Password string
|
|
||||||
AccessKey string
|
|
||||||
Prompt string
|
|
||||||
client *mautrix.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
type Message struct {
|
type Message struct {
|
||||||
EventID id.EventID
|
EventID id.EventID
|
||||||
|
@ -25,7 +18,7 @@ type Conversation struct {
|
||||||
Messages []Message
|
Messages []Message
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConversation(id id.EventID, systemPrompt, question string) *Conversation {
|
func NewConversation(id id.EventID, question string) *Conversation {
|
||||||
return &Conversation{
|
return &Conversation{
|
||||||
Messages: []Message{
|
Messages: []Message{
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,179 @@
|
||||||
|
package bot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/chzyer/readline"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/sashabaranov/go-openai"
|
||||||
|
"maunium.net/go/mautrix"
|
||||||
|
"maunium.net/go/mautrix/crypto/cryptohelper"
|
||||||
|
"maunium.net/go/mautrix/event"
|
||||||
|
"maunium.net/go/mautrix/format"
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Homeserver string
|
||||||
|
UserID string
|
||||||
|
UserAccessKey string
|
||||||
|
UserPassword string
|
||||||
|
DBPath string
|
||||||
|
Pickle string
|
||||||
|
OpenAIKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Matrix struct {
|
||||||
|
config Config
|
||||||
|
readline *readline.Instance
|
||||||
|
client *mautrix.Client
|
||||||
|
cryptoHelper *cryptohelper.CryptoHelper
|
||||||
|
conversations Conversations
|
||||||
|
gptClient *GPT
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg Config) *Matrix {
|
||||||
|
return &Matrix{
|
||||||
|
config: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Matrix) Init() error {
|
||||||
|
client, err := mautrix.NewClient(m.config.Homeserver, id.UserID(m.config.UserID), m.config.UserAccessKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var oei mautrix.OldEventIgnorer
|
||||||
|
oei.Register(client.Syncer.(mautrix.ExtensibleSyncer))
|
||||||
|
m.client = client
|
||||||
|
|
||||||
|
m.client.Log = zerolog.New(zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) {
|
||||||
|
w.TimeFormat = time.Stamp
|
||||||
|
})).With().Timestamp().Logger().Level(zerolog.InfoLevel)
|
||||||
|
|
||||||
|
m.cryptoHelper, err = cryptohelper.NewCryptoHelper(client, []byte(m.config.Pickle), m.config.DBPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.cryptoHelper.LoginAs = &mautrix.ReqLogin{
|
||||||
|
Type: mautrix.AuthTypePassword,
|
||||||
|
Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: m.config.UserID},
|
||||||
|
Password: m.config.UserPassword,
|
||||||
|
}
|
||||||
|
if err := m.cryptoHelper.Init(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.client.Crypto = m.cryptoHelper
|
||||||
|
|
||||||
|
m.gptClient = NewGPT(m.config.OpenAIKey)
|
||||||
|
|
||||||
|
m.conversations = make(Conversations, 0)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Matrix) Run() error {
|
||||||
|
if err := m.client.Sync(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Matrix) Close() error {
|
||||||
|
if err := m.client.Sync(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := m.cryptoHelper.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Matrix) AddEventHandler(eventType event.Type, handler mautrix.EventHandler) {
|
||||||
|
syncer := m.client.Syncer.(*mautrix.DefaultSyncer)
|
||||||
|
syncer.OnEventType(eventType, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Matrix) InviteHandler() (event.Type, mautrix.EventHandler) {
|
||||||
|
return event.StateMember, func(source mautrix.EventSource, evt *event.Event) {
|
||||||
|
if evt.GetStateKey() == m.client.UserID.String() && evt.Content.AsMember().Membership == event.MembershipInvite {
|
||||||
|
_, err := m.client.JoinRoomByID(evt.RoomID)
|
||||||
|
if err == nil {
|
||||||
|
m.client.Log.Info().
|
||||||
|
Str("room_id", evt.RoomID.String()).
|
||||||
|
Str("inviter", evt.Sender.String()).
|
||||||
|
Msg("Joined room after invite")
|
||||||
|
} else {
|
||||||
|
m.client.Log.Error().Err(err).
|
||||||
|
Str("room_id", evt.RoomID.String()).
|
||||||
|
Str("inviter", evt.Sender.String()).
|
||||||
|
Msg("Failed to join room after invite")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Matrix) ResponseHandler() (event.Type, mautrix.EventHandler) {
|
||||||
|
return event.EventMessage, func(source mautrix.EventSource, evt *event.Event) {
|
||||||
|
content := evt.Content.AsMessage()
|
||||||
|
eventID := evt.ID
|
||||||
|
m.client.Log.Info().
|
||||||
|
Str("content", content.Body).
|
||||||
|
Msg("received message")
|
||||||
|
|
||||||
|
conv := m.conversations.FindByEventID(eventID)
|
||||||
|
if conv != nil {
|
||||||
|
m.client.Log.Info().
|
||||||
|
Str("event_id", eventID.String()).
|
||||||
|
Msg("already known message, ignoring")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parentID := id.EventID("")
|
||||||
|
if relatesTo := content.GetRelatesTo(); relatesTo != nil {
|
||||||
|
parentID = relatesTo.GetReplyTo()
|
||||||
|
}
|
||||||
|
if parentID != "" {
|
||||||
|
m.client.Log.Info().Msg("parent found, looking for conversation")
|
||||||
|
conv = m.conversations.FindByEventID(parentID)
|
||||||
|
}
|
||||||
|
if conv != nil {
|
||||||
|
conv.Add(Message{
|
||||||
|
EventID: eventID,
|
||||||
|
ParentID: parentID,
|
||||||
|
Role: openai.ChatMessageRoleUser,
|
||||||
|
Content: content.Body,
|
||||||
|
})
|
||||||
|
m.client.Log.Info().Msg("found parent, appending message to conversation")
|
||||||
|
} else {
|
||||||
|
conv = NewConversation(eventID, content.Body)
|
||||||
|
m.conversations = append(m.conversations, conv)
|
||||||
|
m.client.Log.Info().Msg("no parent found, starting new conversation")
|
||||||
|
}
|
||||||
|
|
||||||
|
if evt.Sender != id.UserID(m.config.UserID) {
|
||||||
|
// get reply from GPT
|
||||||
|
reply, err := m.gptClient.Complete(conv)
|
||||||
|
if err != nil {
|
||||||
|
m.client.Log.Error().Err(err).Msg("OpenAI API returned with ")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
formattedReply := format.RenderMarkdown(reply, true, false)
|
||||||
|
formattedReply.RelatesTo = &event.RelatesTo{
|
||||||
|
InReplyTo: &event.InReplyTo{
|
||||||
|
EventID: eventID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if _, err := m.client.SendMessageEvent(evt.RoomID, event.EventMessage, &formattedReply); err != nil {
|
||||||
|
m.client.Log.Err(err).Msg("failed to send message")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.client.Log.Info().Str("message", fmt.Sprintf("%+v", formattedReply.Body)).Msg("Sent reply")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
go.mod
1
go.mod
|
@ -12,7 +12,6 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.3.2 // indirect
|
|
||||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||||
github.com/tidwall/gjson v1.14.4 // indirect
|
github.com/tidwall/gjson v1.14.4 // indirect
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -1,5 +1,3 @@
|
||||||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
|
||||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||||
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
||||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 216 KiB |
64
main.go
64
main.go
|
@ -1,12 +1,10 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
|
||||||
"ewintr.nl/matrix-bots/bot"
|
"ewintr.nl/matrix-bots/bot"
|
||||||
"github.com/BurntSushi/toml"
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
"golang.org/x/exp/slog"
|
"golang.org/x/exp/slog"
|
||||||
)
|
)
|
||||||
|
@ -14,63 +12,29 @@ import (
|
||||||
func main() {
|
func main() {
|
||||||
logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
|
logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
|
||||||
|
|
||||||
var config bot.Config
|
matrixClient := bot.New(bot.Config{
|
||||||
if _, err := toml.DecodeFile(getParam("CONFIG_PATH", "conf.toml"), &config); err != nil {
|
Homeserver: getParam("MATRIX_HOMESERVER", "http://localhost"),
|
||||||
|
UserID: getParam("MATRIX_USER_ID", "@bot:localhost"),
|
||||||
|
UserPassword: getParam("MATRIX_PASSWORD", "secret"),
|
||||||
|
UserAccessKey: getParam("MATRIX_ACCESS_KEY", "secret"),
|
||||||
|
DBPath: getParam("BOT_DB_PATH", "bot.db"),
|
||||||
|
Pickle: getParam("BOT_PICKLE", "scrambled"),
|
||||||
|
OpenAIKey: getParam("OPENAI_API_KEY", "no key"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := matrixClient.Init(); err != nil {
|
||||||
logger.Error(err.Error())
|
logger.Error(err.Error())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
type Credentials struct {
|
go matrixClient.Run()
|
||||||
Password string
|
|
||||||
AccessKey string
|
|
||||||
}
|
|
||||||
credentials := make(map[string]Credentials)
|
|
||||||
for i := 0; i < len(config.Bots); i++ {
|
|
||||||
user := getParam(fmt.Sprintf("MATRIX_BOT%d_ID", i), "")
|
|
||||||
if user == "" {
|
|
||||||
logger.Error("missing user id", slog.Int("user", i))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
credentials[user] = Credentials{
|
|
||||||
Password: getParam(fmt.Sprintf("MATRIX_BOT%d_PASSWORD", i), ""),
|
|
||||||
AccessKey: getParam(fmt.Sprintf("MATRIX_BOT%d_ACCESSKEY", i), ""),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i, bc := range config.Bots {
|
|
||||||
creds, ok := credentials[bc.UserID]
|
|
||||||
if !ok {
|
|
||||||
logger.Error("missing credentials", slog.Int("user", i))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
config.Bots[i].UserPassword = creds.Password
|
|
||||||
config.Bots[i].UserAccessKey = creds.AccessKey
|
|
||||||
}
|
|
||||||
|
|
||||||
config.OpenAI = bot.ConfigOpenAI{
|
matrixClient.AddEventHandler(matrixClient.InviteHandler())
|
||||||
APIKey: getParam("OPENAI_API_KEY", ""),
|
matrixClient.AddEventHandler(matrixClient.ResponseHandler())
|
||||||
}
|
|
||||||
|
|
||||||
var acceptInvites bool
|
|
||||||
if getParam("MATRIX_ACCEPT_INVITES", "false") == "true" {
|
|
||||||
acceptInvites = true
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info("loaded config", slog.Int("bots", len(config.Bots)))
|
|
||||||
|
|
||||||
for _, bc := range config.Bots {
|
|
||||||
b := bot.New(config.OpenAI.APIKey, bc, logger)
|
|
||||||
if err := b.Init(acceptInvites); err != nil {
|
|
||||||
logger.Error(err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
go b.Run()
|
|
||||||
logger.Info("started bot", slog.String("name", bc.UserDisplayName))
|
|
||||||
}
|
|
||||||
|
|
||||||
done := make(chan os.Signal)
|
done := make(chan os.Signal)
|
||||||
signal.Notify(done, os.Interrupt)
|
signal.Notify(done, os.Interrupt)
|
||||||
<-done
|
<-done
|
||||||
|
|
||||||
logger.Info("service stopped")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getParam(name, def string) string {
|
func getParam(name, def string) string {
|
||||||
|
|
Loading…
Reference in New Issue