discord kommunikation

This commit is contained in:
Christoph K.
2026-03-12 19:16:29 +01:00
parent 92f520101a
commit 5337e7af6f
7 changed files with 249 additions and 16 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
bin/
config.yml
ask

View File

@@ -16,8 +16,15 @@ GOOS=windows GOARCH=amd64 go build -o "$OUT_DIR/ask.exe" ./cmd/ask/
echo " Linux: $OUT_DIR/ask"
echo " Windows: $OUT_DIR/ask.exe"
echo "Baue discord-bot ..."
GOOS=linux GOARCH=amd64 go build -o "$OUT_DIR/discord-bot" ./cmd/discord/
GOOS=windows GOARCH=amd64 go build -o "$OUT_DIR/discord-bot.exe" ./cmd/discord/
echo " Linux: $OUT_DIR/discord-bot"
echo " Windows: $OUT_DIR/discord-bot.exe"
echo ""
echo "Fertig. Nutzung:"
echo " $OUT_DIR/ingest # Markdown importieren"
echo " $OUT_DIR/ingest bild.json # JSON importieren"
echo " $OUT_DIR/ask \"Was sind meine Pläne?\""
echo " $OUT_DIR/discord-bot # Discord-Bot starten"

190
cmd/discord/main.go Normal file
View File

@@ -0,0 +1,190 @@
// 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",
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",
},
}
)
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
for _, cmd := range commands {
_, err := dg.ApplicationCommandCreate(dg.State.User.ID, guildID, cmd)
if err != nil {
log.Printf("⚠️ Slash-Command /%s konnte nicht registriert werden: %v", cmd.Name, err)
} else {
scope := "global"
if guildID != "" {
scope = "guild " + guildID
}
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())
case "ingest":
handleIngest(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) {
// Sofort mit "Denke nach..." antworten (Discord-Timeout: 3s)
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseDeferredChannelMessageWithSource,
})
reply := queryAndFormat(question)
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 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()
}

3
go.mod
View File

@@ -10,6 +10,9 @@ require (
)
require (
github.com/bwmarrin/discordgo v0.29.0 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect

12
go.sum
View File

@@ -1,3 +1,5 @@
github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno=
github.com/bwmarrin/discordgo v0.29.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@@ -8,6 +10,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/qdrant/go-client v1.12.0 h1:KqsIKDAw5iQmxDzRjbzRjhvQ+Igyr7Y84vDCinf1T4M=
github.com/qdrant/go-client v1.12.0/go.mod h1:zFa6t5Y3Oqecoa0aSsGWhMqQWq3x3kTPvm0sMf5qplw=
github.com/sashabaranov/go-openai v1.37.0 h1:hQQowgYm4OXJ1Z/wTrE+XZaO20BYsL0R3uRPSpfNZkY=
@@ -24,12 +28,20 @@ go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=

View File

@@ -21,27 +21,21 @@ type KnowledgeChunk struct {
Source string
}
// Ask sucht relevante Chunks und generiert eine LLM-Antwort per Streaming.
func Ask(question string) {
// AskQuery sucht relevante Chunks und generiert eine LLM-Antwort.
// Gibt die Antwort als String und die verwendeten Quellen zurück.
func AskQuery(question string) (string, []KnowledgeChunk, error) {
ctx := context.Background()
ctx = metadata.AppendToOutgoingContext(ctx, "api-key", config.Cfg.Qdrant.APIKey)
fmt.Printf("🤔 Frage: \"%s\"\n\n", question)
embClient := config.NewEmbeddingClient()
chatClient := config.NewChatClient()
fmt.Println("🔍 Durchsuche lokale Wissensdatenbank...")
chunks := searchKnowledge(ctx, embClient, question)
if len(chunks) == 0 {
fmt.Println("\n❌ Keine relevanten Informationen in der Datenbank gefunden.")
fmt.Println(" Füge mehr Daten mit './bin/ingest' hinzu.")
return
return "", nil, nil
}
contextText := buildContext(chunks)
fmt.Printf("✅ %d relevante Informationen gefunden\n\n", len(chunks))
systemPrompt := `Du bist ein hilfreicher persönlicher Assistent.
Deine Aufgabe ist es, Fragen basierend auf den bereitgestellten Informationen zu beantworten.
@@ -60,9 +54,6 @@ WICHTIGE REGELN:
Basierend auf diesen Informationen, beantworte bitte folgende Frage:
%s`, contextText, question)
fmt.Println("🧠 Generiere Antwort mit lokalem Modell...")
fmt.Println(strings.Repeat("═", 80))
stream, err := chatClient.CreateChatCompletionStream(ctx, openai.ChatCompletionRequest{
Model: config.Cfg.Chat.Model,
Messages: []openai.ChatCompletionMessage{
@@ -73,21 +64,45 @@ Basierend auf diesen Informationen, beantworte bitte folgende Frage:
MaxTokens: 500,
})
if err != nil {
log.Fatalf("LLM Fehler: %v", err)
return "", nil, fmt.Errorf("LLM Fehler: %w", err)
}
defer stream.Close()
fmt.Print("\n💬 Antwort:\n\n")
var answer strings.Builder
for {
response, err := stream.Recv()
if err != nil {
break
}
if len(response.Choices) > 0 {
fmt.Print(response.Choices[0].Delta.Content)
answer.WriteString(response.Choices[0].Delta.Content)
}
}
return answer.String(), chunks, nil
}
// Ask sucht relevante Chunks und gibt Antwort + Quellen auf stdout aus.
func Ask(question string) {
fmt.Printf("🤔 Frage: \"%s\"\n\n", question)
fmt.Println("🔍 Durchsuche lokale Wissensdatenbank...")
answer, chunks, err := AskQuery(question)
if err != nil {
log.Fatalf("❌ %v", err)
}
if len(chunks) == 0 {
fmt.Println("\n❌ Keine relevanten Informationen in der Datenbank gefunden.")
fmt.Println(" Füge mehr Daten mit './bin/ingest' hinzu.")
return
}
fmt.Printf("✅ %d relevante Informationen gefunden\n\n", len(chunks))
fmt.Println("🧠 Generiere Antwort mit lokalem Modell...")
fmt.Println(strings.Repeat("═", 80))
fmt.Print("\n💬 Antwort:\n\n")
fmt.Print(answer)
fmt.Print("\n\n")
fmt.Println(strings.Repeat("═", 80))
fmt.Print("\n📚 Verwendete Quellen:\n")

View File

@@ -31,6 +31,11 @@ type Config struct {
Model string `yaml:"model"`
} `yaml:"chat"`
Discord struct {
Token string `yaml:"token"`
GuildID string `yaml:"guild_id"`
} `yaml:"discord"`
BrainRoot string `yaml:"brain_root"`
TopK uint64 `yaml:"top_k"`
ScoreThreshold float32 `yaml:"score_threshold"`