zwischenstand

This commit is contained in:
Christoph K.
2026-03-20 23:24:56 +01:00
parent b1a576f61e
commit 905981cd1e
25 changed files with 3607 additions and 217 deletions

View File

@@ -3,9 +3,12 @@ package tool
import (
"fmt"
"strings"
"my-brain-importer/internal/agents"
"my-brain-importer/internal/agents/tool/email"
"my-brain-importer/internal/brain"
"my-brain-importer/internal/config"
)
// Agent verteilt Tool-Anfragen an spezialisierte Sub-Agenten.
@@ -41,8 +44,14 @@ func (a *Agent) handleEmail(req agents.Request) agents.Response {
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()
default:
return agents.Response{Text: fmt.Sprintf("❌ Unbekannte Email-Aktion `%s`. Verfügbar: summary, unread, remind", subAction)}
return agents.Response{Text: fmt.Sprintf("❌ Unbekannte Email-Aktion `%s`. Verfügbar: summary, unread, remind, ingest, move, triage", subAction)}
}
if err != nil {
@@ -50,3 +59,144 @@ func (a *Agent) handleEmail(req agents.Request) agents.Response {
}
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}
}
// 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
}