llm mail integration
This commit is contained in:
38
cmd/agenttest/main.go
Normal file
38
cmd/agenttest/main.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"my-brain-importer/internal/agents"
|
||||
"my-brain-importer/internal/agents/task"
|
||||
"my-brain-importer/internal/agents/tool/email"
|
||||
"my-brain-importer/internal/config"
|
||||
)
|
||||
|
||||
func main() {
|
||||
config.LoadConfig()
|
||||
|
||||
fmt.Println("=== Task-Agent Test ===")
|
||||
a := task.New()
|
||||
|
||||
r := a.Handle(agents.Request{Action: agents.ActionAdd, Args: []string{"Synology DSM Update durchfuehren"}})
|
||||
fmt.Println("ADD:", r.Text)
|
||||
|
||||
r = a.Handle(agents.Request{Action: agents.ActionAdd, Args: []string{"Zahnarzt Termin bestaetigen"}})
|
||||
fmt.Println("ADD:", r.Text)
|
||||
|
||||
r = a.Handle(agents.Request{Action: agents.ActionList})
|
||||
fmt.Println("\nLIST:\n" + r.Text)
|
||||
|
||||
fmt.Println("\n=== Email-Agent Test mit Testdaten ===")
|
||||
testMails := []email.Message{
|
||||
{Subject: "KRITISCH: Synology Update verfuegbar", From: "noreply@synology.com", Date: "2026-03-19 14:09"},
|
||||
{Subject: "Rechnung Maerz 2026", From: "buchhaltung@strom.de", Date: "2026-03-18 09:00"},
|
||||
{Subject: "Newsletter GoLang Weekly", From: "newsletter@golangweekly.com", Date: "2026-03-17 18:00"},
|
||||
}
|
||||
result, err := email.SummarizeMessages(testMails, "Fasse zusammen und priorisiere nach Dringlichkeit.")
|
||||
if err != nil {
|
||||
fmt.Println("LLM-Fehler:", err)
|
||||
} else {
|
||||
fmt.Println(result)
|
||||
}
|
||||
}
|
||||
@@ -1,252 +1,509 @@
|
||||
// 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()
|
||||
}
|
||||
// discord – Discord-Bot für my-brain-importer
|
||||
// Unterstützt /ask, /research, /memory, /task, /email, /ingest, /remember und @Mention
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
|
||||
"my-brain-importer/internal/agents"
|
||||
"my-brain-importer/internal/agents/memory"
|
||||
"my-brain-importer/internal/agents/research"
|
||||
"my-brain-importer/internal/agents/task"
|
||||
"my-brain-importer/internal/agents/tool"
|
||||
"my-brain-importer/internal/agents/tool/email"
|
||||
"my-brain-importer/internal/brain"
|
||||
"my-brain-importer/internal/config"
|
||||
)
|
||||
|
||||
var (
|
||||
dg *discordgo.Session
|
||||
botUser *discordgo.User
|
||||
daemonStop = make(chan struct{})
|
||||
|
||||
researchAgent agents.Agent
|
||||
memoryAgent agents.Agent
|
||||
taskAgent agents.Agent
|
||||
toolAgent agents.Agent
|
||||
|
||||
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", Required: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "research",
|
||||
Description: "Wissensdatenbank abfragen (Alias für /ask)",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{Type: discordgo.ApplicationCommandOptionString, Name: "frage", Description: "Die Frage", Required: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "asknobrain",
|
||||
Description: "Stelle eine Frage direkt ans LLM (ohne Wissensdatenbank)",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{Type: discordgo.ApplicationCommandOptionString, Name: "frage", Description: "Die Frage", Required: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ingest",
|
||||
Description: "Importiert Markdown-Notizen aus brain_root in die Wissensdatenbank",
|
||||
},
|
||||
{
|
||||
Name: "remember",
|
||||
Description: "Speichert eine Nachricht in der Wissensdatenbank (Alias für /memory store)",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{Type: discordgo.ApplicationCommandOptionString, Name: "text", Description: "Der Text", Required: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "memory",
|
||||
Description: "Wissen einspeichern",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Name: "store",
|
||||
Description: "Text in die Wissensdatenbank speichern",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{Type: discordgo.ApplicationCommandOptionString, Name: "text", Description: "Der Text", Required: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Name: "ingest",
|
||||
Description: "Markdown-Notizen aus brain_root importieren",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "task",
|
||||
Description: "Aufgabenverwaltung",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Name: "add",
|
||||
Description: "Neuen Task hinzufügen",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{Type: discordgo.ApplicationCommandOptionString, Name: "text", Description: "Der Task", Required: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Name: "list",
|
||||
Description: "Alle Tasks anzeigen",
|
||||
},
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Name: "done",
|
||||
Description: "Task als erledigt markieren",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{Type: discordgo.ApplicationCommandOptionString, Name: "id", Description: "Task-ID (letzte 6 Zeichen)", Required: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Name: "delete",
|
||||
Description: "Task löschen",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{Type: discordgo.ApplicationCommandOptionString, Name: "id", Description: "Task-ID (letzte 6 Zeichen)", Required: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "email",
|
||||
Description: "Emails via IMAP abrufen und analysieren",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Name: "summary",
|
||||
Description: "Zusammenfassung der letzten Emails",
|
||||
},
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Name: "unread",
|
||||
Description: "Ungelesene Emails zusammenfassen",
|
||||
},
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Name: "remind",
|
||||
Description: "Termine und Deadlines aus Emails extrahieren",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
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)")
|
||||
}
|
||||
|
||||
logLevel := slog.LevelInfo
|
||||
if os.Getenv("DEBUG") == "1" {
|
||||
logLevel = slog.LevelDebug
|
||||
fmt.Println("🔍 Debug-Logging aktiv (LLM-Prompts + Antworten werden ausgegeben)")
|
||||
}
|
||||
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
||||
Level: logLevel,
|
||||
})))
|
||||
|
||||
researchAgent = research.New()
|
||||
memoryAgent = memory.New()
|
||||
taskAgent = task.New()
|
||||
toolAgent = tool.New()
|
||||
|
||||
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()
|
||||
go startDaemon()
|
||||
|
||||
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...")
|
||||
close(daemonStop)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
data := i.ApplicationCommandData()
|
||||
slog.Info("Slash-Command", "command", data.Name, "user", getAuthor(i), "channel", i.ChannelID)
|
||||
switch data.Name {
|
||||
case "ask", "research":
|
||||
question := data.Options[0].StringValue()
|
||||
handleAgentResponse(s, i, func() agents.Response {
|
||||
return researchAgent.Handle(agents.Request{Action: agents.ActionQuery, Args: []string{question}})
|
||||
})
|
||||
|
||||
case "asknobrain":
|
||||
handleAskNoBrain(s, i, data.Options[0].StringValue())
|
||||
|
||||
case "ingest":
|
||||
handleAgentResponse(s, i, func() agents.Response {
|
||||
return memoryAgent.Handle(agents.Request{Action: agents.ActionIngest})
|
||||
})
|
||||
|
||||
case "remember":
|
||||
text := data.Options[0].StringValue()
|
||||
author := getAuthor(i)
|
||||
source := fmt.Sprintf("discord/#%s", i.ChannelID)
|
||||
handleAgentResponse(s, i, func() agents.Response {
|
||||
return memoryAgent.Handle(agents.Request{Action: agents.ActionStore, Args: []string{text}, Author: author, Source: source})
|
||||
})
|
||||
|
||||
case "memory":
|
||||
handleMemoryCommand(s, i)
|
||||
|
||||
case "task":
|
||||
handleTaskCommand(s, i)
|
||||
|
||||
case "email":
|
||||
handleEmailCommand(s, i)
|
||||
}
|
||||
}
|
||||
|
||||
func handleMemoryCommand(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
sub := i.ApplicationCommandData().Options[0]
|
||||
switch sub.Name {
|
||||
case "store":
|
||||
text := sub.Options[0].StringValue()
|
||||
author := getAuthor(i)
|
||||
source := fmt.Sprintf("discord/#%s", i.ChannelID)
|
||||
handleAgentResponse(s, i, func() agents.Response {
|
||||
return memoryAgent.Handle(agents.Request{Action: agents.ActionStore, Args: []string{text}, Author: author, Source: source})
|
||||
})
|
||||
case "ingest":
|
||||
handleAgentResponse(s, i, func() agents.Response {
|
||||
return memoryAgent.Handle(agents.Request{Action: agents.ActionIngest})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func handleTaskCommand(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
sub := i.ApplicationCommandData().Options[0]
|
||||
req := agents.Request{Action: sub.Name} // sub.Name ist bereits der Action-String (add/list/done/delete)
|
||||
if len(sub.Options) > 0 {
|
||||
req.Args = []string{sub.Options[0].StringValue()}
|
||||
}
|
||||
handleAgentResponse(s, i, func() agents.Response {
|
||||
return taskAgent.Handle(req)
|
||||
})
|
||||
}
|
||||
|
||||
func handleEmailCommand(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
sub := i.ApplicationCommandData().Options[0]
|
||||
handleAgentResponse(s, i, func() agents.Response {
|
||||
return toolAgent.Handle(agents.Request{Action: agents.ActionEmail, Args: []string{sub.Name}})
|
||||
})
|
||||
}
|
||||
|
||||
func handleAskNoBrain(s *discordgo.Session, i *discordgo.InteractionCreate, question string) {
|
||||
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseDeferredChannelMessageWithSource,
|
||||
})
|
||||
answer, err := brain.ChatDirect(question)
|
||||
var reply string
|
||||
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})
|
||||
}
|
||||
|
||||
// handleAgentResponse sendet erst Defer an Discord, führt dann fn() aus und editiert die Antwort.
|
||||
// So wird das 3-Sekunden-Timeout von Discord nie überschritten.
|
||||
func handleAgentResponse(s *discordgo.Session, i *discordgo.InteractionCreate, fn func() agents.Response) {
|
||||
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseDeferredChannelMessageWithSource,
|
||||
})
|
||||
resp := fn()
|
||||
if resp.Error != nil {
|
||||
slog.Error("Agent-Fehler", "fehler", resp.Error)
|
||||
}
|
||||
msg := resp.Text
|
||||
if msg == "" && resp.Error != nil {
|
||||
msg = fmt.Sprintf("❌ Fehler: %v", resp.Error)
|
||||
}
|
||||
s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{Content: &msg})
|
||||
}
|
||||
|
||||
func onMessage(s *discordgo.Session, m *discordgo.MessageCreate) {
|
||||
if m.Author.Bot || botUser == nil {
|
||||
return
|
||||
}
|
||||
|
||||
mentioned := false
|
||||
for _, u := range m.Mentions {
|
||||
if u.ID == botUser.ID {
|
||||
mentioned = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !mentioned {
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
slog.Info("Mention", "user", m.Author.Username, "channel", m.ChannelID, "nachricht", question)
|
||||
s.ChannelTyping(m.ChannelID)
|
||||
resp := routeMessage(question, getAuthorFromMessage(m))
|
||||
if resp.Error != nil {
|
||||
slog.Error("Mention-Fehler", "fehler", resp.Error)
|
||||
}
|
||||
s.ChannelMessageSendReply(m.ChannelID, resp.Text, m.Reference())
|
||||
}
|
||||
|
||||
// SendMessage schickt eine Nachricht proaktiv in einen Channel (für Daemon-Nutzung).
|
||||
func SendMessage(channelID, text string) error {
|
||||
if dg == nil {
|
||||
return fmt.Errorf("Discord-Session nicht initialisiert")
|
||||
}
|
||||
_, err := dg.ChannelMessageSend(channelID, text)
|
||||
return err
|
||||
}
|
||||
|
||||
// routeMessage leitet eine @mention-Nachricht an den passenden Agenten weiter.
|
||||
// Unterstützte Präfixe: "email ...", "task ...", "remember ...", sonst Research.
|
||||
func routeMessage(text, author string) agents.Response {
|
||||
words := strings.Fields(text)
|
||||
if len(words) == 0 {
|
||||
return agents.Response{Text: "Stell mir eine Frage oder nutze: `email summary`, `email unread`, `email remind`, `task list`, `task add <text>`, `task done <id>`, `remember <text>`"}
|
||||
}
|
||||
|
||||
cmd := strings.ToLower(words[0])
|
||||
args := words[1:]
|
||||
|
||||
switch cmd {
|
||||
case "email":
|
||||
sub := "summary"
|
||||
if len(args) > 0 {
|
||||
sub = strings.ToLower(args[0])
|
||||
}
|
||||
return toolAgent.Handle(agents.Request{Action: agents.ActionEmail, Args: []string{sub}})
|
||||
|
||||
case "task":
|
||||
action := "list"
|
||||
if len(args) > 0 {
|
||||
action = strings.ToLower(args[0])
|
||||
args = args[1:]
|
||||
}
|
||||
return taskAgent.Handle(agents.Request{Action: action, Args: args})
|
||||
|
||||
case "remember":
|
||||
return memoryAgent.Handle(agents.Request{
|
||||
Action: agents.ActionStore,
|
||||
Args: args,
|
||||
Author: author,
|
||||
Source: "discord/mention",
|
||||
})
|
||||
|
||||
default:
|
||||
return researchAgent.Handle(agents.Request{Action: agents.ActionQuery, Args: words})
|
||||
}
|
||||
}
|
||||
|
||||
func getAuthorFromMessage(m *discordgo.MessageCreate) string {
|
||||
if m.Author != nil {
|
||||
return m.Author.Username
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func getAuthor(i *discordgo.InteractionCreate) string {
|
||||
if i.Member != nil {
|
||||
return i.Member.User.Username
|
||||
}
|
||||
if i.User != nil {
|
||||
return i.User.Username
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// startDaemon läuft als Goroutine im Bot-Prozess und sendet proaktive Benachrichtigungen.
|
||||
func startDaemon() {
|
||||
channelID := config.Cfg.Daemon.ChannelID
|
||||
if channelID == "" {
|
||||
log.Println("⚙️ Daemon inaktiv: daemon.channel_id nicht konfiguriert")
|
||||
return
|
||||
}
|
||||
|
||||
emailInterval := time.Duration(config.Cfg.Daemon.EmailIntervalMin) * time.Minute
|
||||
if emailInterval == 0 {
|
||||
emailInterval = 30 * time.Minute
|
||||
}
|
||||
reminderHour := config.Cfg.Daemon.TaskReminderHour
|
||||
if reminderHour == 0 {
|
||||
reminderHour = 8
|
||||
}
|
||||
|
||||
log.Printf("⚙️ Daemon aktiv: Email-Check alle %v, Task-Reminder täglich um %02d:00", emailInterval, reminderHour)
|
||||
|
||||
emailTicker := time.NewTicker(emailInterval)
|
||||
defer emailTicker.Stop()
|
||||
|
||||
taskTimer := scheduleDaily(reminderHour, 0)
|
||||
defer taskTimer.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-daemonStop:
|
||||
slog.Info("Daemon gestoppt")
|
||||
return
|
||||
|
||||
case <-emailTicker.C:
|
||||
slog.Info("Daemon: Email-Check gestartet")
|
||||
notify, err := email.SummarizeUnread()
|
||||
if err != nil {
|
||||
slog.Error("Daemon Email-Fehler", "fehler", err)
|
||||
dg.ChannelMessageSend(channelID, fmt.Sprintf("⚠️ Email-Check fehlgeschlagen: %v", err))
|
||||
continue
|
||||
}
|
||||
if notify != "📭 Keine ungelesenen Emails." {
|
||||
slog.Info("Daemon: Neue Emails gefunden, sende Zusammenfassung")
|
||||
dg.ChannelMessageSend(channelID, "📧 **Neue Emails:**\n\n"+notify)
|
||||
} else {
|
||||
slog.Info("Daemon: Keine neuen Emails")
|
||||
}
|
||||
|
||||
case <-taskTimer.C:
|
||||
slog.Info("Daemon: Task-Reminder gestartet")
|
||||
store := task.NewStore()
|
||||
open, err := store.OpenTasks()
|
||||
if err != nil {
|
||||
slog.Error("Daemon Task-Fehler", "fehler", err)
|
||||
continue
|
||||
}
|
||||
if len(open) > 0 {
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "📋 **Tägliche Task-Erinnerung** – %d offene Tasks:\n\n", len(open))
|
||||
for _, t := range open {
|
||||
shortID := t.ID
|
||||
if len(shortID) > 6 {
|
||||
shortID = shortID[len(shortID)-6:]
|
||||
}
|
||||
fmt.Fprintf(&sb, "⬜ `%s` – %s\n", shortID, t.Text)
|
||||
}
|
||||
dg.ChannelMessageSend(channelID, sb.String())
|
||||
slog.Info("Daemon: Task-Reminder gesendet", "offene_tasks", len(open))
|
||||
}
|
||||
taskTimer.Stop()
|
||||
taskTimer = scheduleDaily(reminderHour, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// scheduleDaily gibt einen Timer zurück, der zum nächsten Auftreten von hour:minute feuert.
|
||||
func scheduleDaily(hour, minute int) *time.Timer {
|
||||
now := time.Now()
|
||||
next := time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, now.Location())
|
||||
if !next.After(now) {
|
||||
next = next.Add(24 * time.Hour)
|
||||
}
|
||||
d := next.Sub(now)
|
||||
log.Printf("⏰ Nächster Task-Reminder in %v (um %02d:%02d)", d.Round(time.Minute), hour, minute)
|
||||
return time.NewTimer(d)
|
||||
}
|
||||
|
||||
110
cmd/mailtest/main.go
Normal file
110
cmd/mailtest/main.go
Normal file
@@ -0,0 +1,110 @@
|
||||
// mailtest – IMAP-Verbindungstest und LLM-Zusammenfassungstest
|
||||
//
|
||||
// Flags:
|
||||
//
|
||||
// -llm-only Überspringt IMAP, testet LLM mit eingebetteten Testdaten
|
||||
// -unread Holt nur ungelesene Emails (statt letzte 5)
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"my-brain-importer/internal/agents/tool/email"
|
||||
"my-brain-importer/internal/config"
|
||||
)
|
||||
|
||||
// testEmails sind eingebettete Beispieldaten für den LLM-Test ohne IMAP.
|
||||
var testEmails = []email.Message{
|
||||
{Subject: "KRITISCH: Server down in Produktion", From: "monitoring@company.com", Date: "2026-03-19 09:00"},
|
||||
{Subject: "Meeting morgen 10 Uhr – Projektreview Q1", From: "chef@company.com", Date: "2026-03-19 08:30"},
|
||||
{Subject: "Rechnung Nr. 2026-042 fällig bis 25.03.", From: "buchhaltung@lieferant.de", Date: "2026-03-18 14:00"},
|
||||
{Subject: "Re: Urlaubsantrag genehmigt", From: "hr@company.com", Date: "2026-03-18 11:00"},
|
||||
{Subject: "Newsletter: Neue Features in Go 1.26", From: "newsletter@golangweekly.com", Date: "2026-03-17 18:00"},
|
||||
{Subject: "Amazon: Ihre Bestellung wurde versendet", From: "no-reply@amazon.de", Date: "2026-03-17 10:00"},
|
||||
{Subject: "Erinnerung: Zahnarzttermin 21.03. um 15:00", From: "praxis@zahnarzt.de", Date: "2026-03-16 09:00"},
|
||||
}
|
||||
|
||||
func main() {
|
||||
llmOnly := flag.Bool("llm-only", false, "Überspringe IMAP, teste LLM mit eingebetteten Testdaten")
|
||||
unread := flag.Bool("unread", false, "Hole nur ungelesene Emails")
|
||||
verbose := flag.Bool("v", false, "Verbose Logging (Debug-Level)")
|
||||
flag.Parse()
|
||||
|
||||
config.LoadConfig()
|
||||
|
||||
// Logging konfigurieren
|
||||
logLevel := slog.LevelInfo
|
||||
if *verbose {
|
||||
logLevel = slog.LevelDebug
|
||||
}
|
||||
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: logLevel})))
|
||||
|
||||
if *llmOnly {
|
||||
runLLMTest()
|
||||
return
|
||||
}
|
||||
|
||||
cfg := config.Cfg.Email
|
||||
fmt.Printf("🔌 Verbinde mit %s:%d (TLS=%v StartTLS=%v) als %s ...\n",
|
||||
cfg.Host, cfg.Port, cfg.TLS, cfg.StartTLS, cfg.User)
|
||||
|
||||
cl, err := email.Connect()
|
||||
if err != nil {
|
||||
log.Fatalf("❌ Verbindung fehlgeschlagen: %v", err)
|
||||
}
|
||||
defer cl.Close()
|
||||
fmt.Println("✅ Login erfolgreich!")
|
||||
|
||||
var msgs []email.Message
|
||||
if *unread {
|
||||
fmt.Println("📥 Hole ungelesene Emails...")
|
||||
msgs, err = cl.FetchUnread()
|
||||
} else {
|
||||
fmt.Println("📥 Hole die letzten 5 Emails...")
|
||||
msgs, err = cl.FetchRecent(5)
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("❌ Fetch fehlgeschlagen: %v", err)
|
||||
}
|
||||
if len(msgs) == 0 {
|
||||
fmt.Println("📭 Keine Emails gefunden.")
|
||||
return
|
||||
}
|
||||
for i, m := range msgs {
|
||||
fmt.Printf("[%d] %s | Von: %s | %s\n", i+1, m.Date, m.From, m.Subject)
|
||||
}
|
||||
|
||||
fmt.Printf("\n🤖 Teste LLM-Zusammenfassung (Modell: %s)...\n", effectiveModel())
|
||||
summary, err := email.Summarize()
|
||||
if err != nil {
|
||||
log.Fatalf("❌ Fehler: %v", err)
|
||||
}
|
||||
fmt.Println(summary)
|
||||
}
|
||||
|
||||
func runLLMTest() {
|
||||
fmt.Printf("🧪 LLM-Testmodus mit %d eingebetteten Testdaten\n", len(testEmails))
|
||||
fmt.Printf(" Modell: %s\n\n", effectiveModel())
|
||||
for i, m := range testEmails {
|
||||
fmt.Printf("[%d] %s | %s | %s\n", i+1, m.Date, m.From, m.Subject)
|
||||
}
|
||||
|
||||
fmt.Println("\n🤖 Starte LLM-Zusammenfassung...")
|
||||
result, err := email.SummarizeMessages(testEmails, "Fasse diese Emails zusammen. Priorisiere nach Dringlichkeit und Wichtigkeit.")
|
||||
if err != nil {
|
||||
log.Fatalf("❌ LLM-Fehler: %v", err)
|
||||
}
|
||||
fmt.Println("\n--- Ergebnis ---")
|
||||
fmt.Println(result)
|
||||
}
|
||||
|
||||
func effectiveModel() string {
|
||||
if config.Cfg.Email.Model != "" {
|
||||
return config.Cfg.Email.Model
|
||||
}
|
||||
return config.Cfg.Chat.Model
|
||||
}
|
||||
Reference in New Issue
Block a user