discord kommunikation
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
bin/
|
||||
config.yml
|
||||
ask
|
||||
|
||||
7
build.sh
7
build.sh
@@ -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
190
cmd/discord/main.go
Normal 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
3
go.mod
@@ -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
12
go.sum
@@ -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=
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"`
|
||||
|
||||
Reference in New Issue
Block a user