Files
ai-agent/cmd/discord/main.go
2026-03-12 19:49:50 +01:00

253 lines
6.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// discord Discord-Bot für my-brain-importer
// Unterstützt /ask, /ingest und @Mention
package main
import (
"fmt"
"log"
"os"
"os/signal"
"strings"
"syscall"
"github.com/bwmarrin/discordgo"
"my-brain-importer/internal/brain"
"my-brain-importer/internal/config"
)
var (
dg *discordgo.Session
botUser *discordgo.User
commands = []*discordgo.ApplicationCommand{
{
Name: "ask",
Description: "Stelle eine Frage an deinen AI Brain (mit Wissensdatenbank)",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "frage",
Description: "Die Frage, die du stellen möchtest",
Required: true,
},
},
},
{
Name: "asknobrain",
Description: "Stelle eine Frage direkt ans LLM (ohne Wissensdatenbank)",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "frage",
Description: "Die Frage, die du stellen möchtest",
Required: true,
},
},
},
{
Name: "ingest",
Description: "Importiert Markdown-Notizen aus brain_root in die Wissensdatenbank",
},
{
Name: "remember",
Description: "Speichert eine Nachricht in der Wissensdatenbank",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "text",
Description: "Der Text, der gespeichert werden soll",
Required: true,
},
},
},
}
)
func main() {
config.LoadConfig()
token := config.Cfg.Discord.Token
if token == "" || token == "dein-discord-bot-token" {
log.Fatal("❌ Kein Discord-Token in config.yml konfiguriert (discord.token)")
}
var err error
dg, err = discordgo.New("Bot " + token)
if err != nil {
log.Fatalf("❌ Discord-Session konnte nicht erstellt werden: %v", err)
}
dg.AddHandler(onReady)
dg.AddHandler(onInteraction)
dg.AddHandler(onMessage)
dg.Identify.Intents = discordgo.IntentsGuilds |
discordgo.IntentsGuildMessages |
discordgo.IntentMessageContent
if err = dg.Open(); err != nil {
log.Fatalf("❌ Verbindung zu Discord fehlgeschlagen: %v", err)
}
defer dg.Close()
registerCommands()
fmt.Println("✅ Bot läuft. Drücke CTRL+C zum Beenden.")
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
<-stop
fmt.Println("\n👋 Bot wird beendet...")
}
func onReady(s *discordgo.Session, r *discordgo.Ready) {
botUser = r.User
fmt.Printf("✅ Eingeloggt als %s#%s\n", r.User.Username, r.User.Discriminator)
}
func registerCommands() {
guildID := config.Cfg.Discord.GuildID
registered, err := dg.ApplicationCommandBulkOverwrite(dg.State.User.ID, guildID, commands)
if err != nil {
log.Fatalf("❌ Slash-Commands konnten nicht registriert werden: %v", err)
}
scope := "global"
if guildID != "" {
scope = "guild " + guildID
}
for _, cmd := range registered {
fmt.Printf("📝 Slash-Command /%s registriert (%s)\n", cmd.Name, scope)
}
}
func onInteraction(s *discordgo.Session, i *discordgo.InteractionCreate) {
if i.Type != discordgo.InteractionApplicationCommand {
return
}
switch i.ApplicationCommandData().Name {
case "ask":
handleAsk(s, i, i.ApplicationCommandData().Options[0].StringValue(), true)
case "asknobrain":
handleAsk(s, i, i.ApplicationCommandData().Options[0].StringValue(), false)
case "ingest":
handleIngest(s, i)
case "remember":
handleRemember(s, i)
}
}
func onMessage(s *discordgo.Session, m *discordgo.MessageCreate) {
if m.Author.Bot {
return
}
if botUser == nil {
return
}
mentioned := false
for _, u := range m.Mentions {
if u.ID == botUser.ID {
mentioned = true
break
}
}
if !mentioned {
return
}
// Mention aus der Nachricht entfernen
question := strings.TrimSpace(
strings.ReplaceAll(m.Content, "<@"+botUser.ID+">", ""),
)
if question == "" {
s.ChannelMessageSend(m.ChannelID, "Stell mir eine Frage! Beispiel: @Brain Was sind meine TODOs?")
return
}
s.ChannelTyping(m.ChannelID)
reply := queryAndFormat(question)
s.ChannelMessageSendReply(m.ChannelID, reply, m.Reference())
}
func handleAsk(s *discordgo.Session, i *discordgo.InteractionCreate, question string, useBrain bool) {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseDeferredChannelMessageWithSource,
})
var reply string
if useBrain {
reply = queryAndFormat(question)
} else {
answer, err := brain.ChatDirect(question)
if err != nil {
reply = fmt.Sprintf("❌ Fehler: %v", err)
} else {
reply = fmt.Sprintf("💬 **Antwort:** _%s_\n\n%s", question, answer)
}
}
s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
Content: &reply,
})
}
func handleIngest(s *discordgo.Session, i *discordgo.InteractionCreate) {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseDeferredChannelMessageWithSource,
})
fmt.Println("📥 Ingest gestartet via Discord...")
brain.RunIngest(config.Cfg.BrainRoot)
msg := fmt.Sprintf("✅ Ingest abgeschlossen! Quelle: `%s`", config.Cfg.BrainRoot)
s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
Content: &msg,
})
}
func handleRemember(s *discordgo.Session, i *discordgo.InteractionCreate) {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseDeferredChannelMessageWithSource,
})
text := i.ApplicationCommandData().Options[0].StringValue()
var author string
if i.Member != nil {
author = i.Member.User.Username
} else if i.User != nil {
author = i.User.Username
} else {
author = "unknown"
}
source := fmt.Sprintf("discord/#%s", i.ChannelID)
err := brain.IngestChatMessage(text, author, source)
var msg string
if err != nil {
msg = fmt.Sprintf("❌ Fehler beim Speichern: %v", err)
} else {
msg = fmt.Sprintf("✅ Gespeichert: _%s_", text)
}
s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{Content: &msg})
}
func queryAndFormat(question string) string {
answer, chunks, err := brain.AskQuery(question)
if err != nil {
return fmt.Sprintf("❌ Fehler: %v", err)
}
if len(chunks) == 0 {
return "❌ Keine relevanten Informationen in der Datenbank gefunden.\nFüge mehr Daten mit `/ingest` hinzu."
}
var sb strings.Builder
fmt.Fprintf(&sb, "💬 **Antwort auf:** _%s_\n\n", question)
sb.WriteString(answer)
sb.WriteString("\n\n📚 **Quellen:**\n")
for _, chunk := range chunks {
fmt.Fprintf(&sb, "• %.1f%% %s\n", chunk.Score*100, chunk.Source)
}
return sb.String()
}