// tool/agent.go – Tool-Agent: Dispatcher für externe Tools (Email, ...) package tool import ( "fmt" "strconv" "strings" "my-brain-importer/internal/agents" "my-brain-importer/internal/agents/tool/email" "my-brain-importer/internal/brain" "my-brain-importer/internal/config" "my-brain-importer/internal/triage" ) // Agent verteilt Tool-Anfragen an spezialisierte Sub-Agenten. type Agent struct{} func New() *Agent { return &Agent{} } // Handle unterstützt: email func (a *Agent) Handle(req agents.Request) agents.Response { switch req.Action { case agents.ActionEmail: return a.handleEmail(req) default: return agents.Response{Text: "❌ Unbekannte Tool-Aktion. Verfügbar: email"} } } func (a *Agent) handleEmail(req agents.Request) agents.Response { subAction := agents.ActionEmailSummary if len(req.Args) > 0 { subAction = req.Args[0] } var ( result string err error ) switch subAction { case agents.ActionEmailSummary: result, err = email.Summarize() case agents.ActionEmailUnread: result, err = email.SummarizeUnread() case agents.ActionEmailRemind: result, err = email.ExtractReminders() case agents.ActionEmailIngest: return a.handleEmailIngest(req) case agents.ActionEmailMove: return a.handleEmailMove(req) case agents.ActionEmailTriage: return a.handleEmailTriage() case agents.ActionEmailTriageHistory: return a.handleTriageHistory(req) case agents.ActionEmailTriageCorrect: return a.handleTriageCorrect(req) case agents.ActionEmailTriageSearch: return a.handleTriageSearch(req) default: return agents.Response{Text: fmt.Sprintf("❌ Unbekannte Email-Aktion `%s`. Verfügbar: summary, unread, remind, ingest, move, triage, triage-history, triage-correct, triage-search", subAction)} } if err != nil { return agents.Response{Error: err, Text: fmt.Sprintf("❌ Email-Fehler: %v", err)} } return agents.Response{Text: "📧 **Email-Analyse:**\n\n" + result} } // handleEmailIngest importiert Emails aus einem IMAP-Ordner in Qdrant. // Args[1] = Ordnername (Standard: "Archiv") func (a *Agent) handleEmailIngest(req agents.Request) agents.Response { folder := "Archiv" if len(req.Args) > 1 && req.Args[1] != "" { folder = req.Args[1] } accounts := config.AllEmailAccounts() if len(accounts) == 0 { return agents.Response{Text: "❌ Kein Email-Account konfiguriert."} } total := 0 var errs []string for _, acc := range accounts { n, err := brain.IngestEmailFolder(acc, folder, 500) if err != nil { errs = append(errs, fmt.Sprintf("%s: %v", acc.Name, err)) continue } total += n } if len(errs) > 0 && total == 0 { return agents.Response{Text: fmt.Sprintf("❌ Email-Ingest fehlgeschlagen:\n%s", joinLines(errs))} } msg := fmt.Sprintf("✅ **Email-Ingest abgeschlossen:** %d Emails aus `%s` in die Wissensdatenbank importiert.", total, folder) if len(errs) > 0 { msg += "\n⚠️ Fehler bei einigen Accounts:\n" + joinLines(errs) } return agents.Response{Text: msg} } // handleEmailMove verschiebt alle ungelesenen Emails in einen konfigurierten Archivordner. // Args[1] = Zielordner-Name (aus archive_folders in config oder Legacy: 2Jahre/5Jahre/Archiv) func (a *Agent) handleEmailMove(req agents.Request) agents.Response { if len(req.Args) < 2 || req.Args[1] == "" { return agents.Response{Text: "❌ Zielordner fehlt. " + buildMoveFoldersHint()} } dest := req.Args[1] imapFolder, ok := resolveArchiveFolder(dest) if !ok { return agents.Response{Text: fmt.Sprintf("❌ Unbekannter Ordner `%s`. %s", dest, buildMoveFoldersHint())} } accounts := config.AllEmailAccounts() if len(accounts) == 0 { return agents.Response{Text: "❌ Kein Email-Account konfiguriert."} } total := 0 var errs []string for _, acc := range accounts { n, err := email.MoveUnread(acc, imapFolder) if err != nil { errs = append(errs, fmt.Sprintf("%s: %v", acc.Name, err)) continue } total += n } if len(errs) > 0 && total == 0 { return agents.Response{Text: fmt.Sprintf("❌ Verschieben fehlgeschlagen:\n%s", joinLines(errs))} } if total == 0 { return agents.Response{Text: fmt.Sprintf("📭 Keine ungelesenen Emails zum Verschieben nach `%s`.", imapFolder)} } msg := fmt.Sprintf("✅ **%d Email(s)** nach `%s` verschoben.", total, imapFolder) if len(errs) > 0 { msg += "\n⚠️ Fehler:\n" + joinLines(errs) } return agents.Response{Text: msg} } // handleEmailTriage klassifiziert die letzten 10 Emails aller Accounts und verschiebt sie. func (a *Agent) handleEmailTriage() agents.Response { result, err := email.TriageRecentAllAccounts(10) if err != nil { return agents.Response{Error: err, Text: fmt.Sprintf("❌ Triage fehlgeschlagen: %v", err)} } return agents.Response{Text: "🗂️ **Email-Triage (letzte 10 Emails):**\n\n" + result} } // handleTriageHistory zeigt die letzten N Triage-Entscheidungen. func (a *Agent) handleTriageHistory(req agents.Request) agents.Response { limit := uint32(10) if len(req.Args) > 1 && req.Args[1] != "" { if n, err := strconv.ParseUint(req.Args[1], 10, 32); err == nil && n > 0 { limit = uint32(n) } } results, err := triage.ListRecent(limit) if err != nil { return agents.Response{Error: err, Text: fmt.Sprintf("❌ Triage-History fehlgeschlagen: %v", err)} } if len(results) == 0 { return agents.Response{Text: "📭 Keine Triage-Entscheidungen gespeichert."} } var sb strings.Builder fmt.Fprintf(&sb, "🗂️ **Triage-History (%d Einträge):**\n\n", len(results)) for i, r := range results { fmt.Fprintf(&sb, "**%d.** %s\n", i+1, r.Text) } return agents.Response{Text: sb.String()} } // handleTriageCorrect korrigiert eine Triage-Entscheidung (wichtig↔unwichtig). func (a *Agent) handleTriageCorrect(req agents.Request) agents.Response { if len(req.Args) < 2 || req.Args[1] == "" { return agents.Response{Text: "❌ Betreff fehlt. Beispiel: `/email triage-correct Newsletter`"} } query := strings.Join(req.Args[1:], " ") msg, err := triage.CorrectDecision(query) if err != nil { return agents.Response{Error: err, Text: fmt.Sprintf("❌ Korrektur fehlgeschlagen: %v", err)} } return agents.Response{Text: "✅ " + msg} } // handleTriageSearch sucht ähnliche Triage-Entscheidungen. func (a *Agent) handleTriageSearch(req agents.Request) agents.Response { if len(req.Args) < 2 || req.Args[1] == "" { return agents.Response{Text: "❌ Suchbegriff fehlt. Beispiel: `/email triage-search Newsletter`"} } query := strings.Join(req.Args[1:], " ") results, err := triage.SearchExtended(query, 10) if err != nil { return agents.Response{Error: err, Text: fmt.Sprintf("❌ Triage-Suche fehlgeschlagen: %v", err)} } if len(results) == 0 { return agents.Response{Text: "📭 Keine ähnlichen Triage-Entscheidungen gefunden."} } var sb strings.Builder fmt.Fprintf(&sb, "🔍 **Triage-Suche** (query: `%s`, %d Treffer):\n\n", query, len(results)) for i, r := range results { fmt.Fprintf(&sb, "**%d.** [%.0f%%] %s\n", i+1, r.Score*100, r.Text) } return agents.Response{Text: sb.String()} } // ResolveArchiveFolder ist die exportierte Version von resolveArchiveFolder für den Discord-Layer. func ResolveArchiveFolder(name string) (imapFolder string, ok bool) { return resolveArchiveFolder(name) } // resolveArchiveFolder sucht den IMAP-Ordnernamen für einen Anzeigenamen aus der Config. // Fallback: Legacy-Hardcoding für 2Jahre/5Jahre/Archiv wenn keine archive_folders konfiguriert. func resolveArchiveFolder(name string) (imapFolder string, ok bool) { for _, acc := range config.AllEmailAccounts() { for _, af := range acc.ArchiveFolders { if strings.EqualFold(af.Name, name) || strings.EqualFold(af.IMAPFolder, name) { return af.IMAPFolder, true } } } // Legacy-Fallback für Konfigurationen ohne archive_folders legacy := map[string]string{ "2jahre": "2Jahre", "5jahre": "5Jahre", "archiv": "Archiv", } if canonical, found := legacy[strings.ToLower(name)]; found { return canonical, true } return "", false } // buildMoveFoldersHint gibt eine Hinweis-Nachricht mit verfügbaren Archivordnern zurück. func buildMoveFoldersHint() string { seen := map[string]bool{} var names []string for _, acc := range config.AllEmailAccounts() { for _, af := range acc.ArchiveFolders { key := strings.ToLower(af.Name) if !seen[key] { seen[key] = true names = append(names, fmt.Sprintf("`%s`", af.Name)) } } } if len(names) == 0 { return "Verfügbar: `2Jahre`, `5Jahre`, `Archiv`" } return fmt.Sprintf("Verfügbar: %s", strings.Join(names, ", ")) } func joinLines(lines []string) string { result := "" for _, l := range lines { result += "• " + l + "\n" } return result }