Email-Triage: Lernen aus IMAP-Ordnern, manuelle Korrektur, reichere Daten

- Automatisches Triage-Lernen aus Archiv-Ordnern im Nacht-Ingest:
  retention_days=0 (Archiv) → wichtig, retention_days>0 → unwichtig
- Drei neue Discord-Commands: /email triage-history, triage-correct, triage-search
- StoreDecision speichert jetzt Datum + Body-Zusammenfassung (max 200 Zeichen)
- MIME-Multipart-Parsing mit PDF-Attachment-Extraktion (FetchWithBodyAndAttachments)
- Deterministische IDs basierend auf Absender+Betreff (idempotente Upserts)
- Rueckwaertskompatibles Parsing fuer alte Triage-Eintraege ohne Datum/Body

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Christoph K.
2026-03-21 14:13:55 +01:00
parent 905981cd1e
commit b6b451779d
6 changed files with 695 additions and 14 deletions

View File

@@ -328,7 +328,8 @@ func ClassifyImportance(msg Message, model string) bool {
)
// Entscheidung für künftiges Lernen in Qdrant speichern
if err := triage.StoreDecision(msg.Subject, msg.From, isImportant); err != nil {
// Body ist bei ClassifyImportance nicht verfügbar (nur Envelope), daher leer.
if err := triage.StoreDecision(msg.Subject, msg.From, msg.Date, "", isImportant); err != nil {
slog.Warn("[Triage] Entscheidung nicht gespeichert", "fehler", err)
}
@@ -646,3 +647,64 @@ func fallbackList(msgs []Message) string {
}
return sb.String()
}
// LearnFromFolders scannt die Archiv-Ordner eines Accounts und speichert Triage-Entscheidungen in Qdrant.
// Ordner mit retention_days == 0 (Archiv/dauerhaft) → wichtig, retention_days > 0 → unwichtig.
// Pro Ordner werden maximal die letzten 50 Emails verarbeitet.
func LearnFromFolders(acc config.EmailAccount) (wichtig, unwichtig int, err error) {
if len(acc.ArchiveFolders) == 0 {
return 0, 0, nil
}
cl, err := ConnectAccount(acc)
if err != nil {
return 0, 0, fmt.Errorf("IMAP verbinden: %w", err)
}
defer cl.Close()
for _, af := range acc.ArchiveFolders {
isImportant := af.RetentionDays == 0
msgs, err := cl.FetchWithBodyAndAttachments(af.IMAPFolder, 50)
if err != nil {
slog.Warn("[Triage-Learn] Ordner nicht lesbar", "ordner", af.IMAPFolder, "fehler", err)
continue
}
for _, m := range msgs {
if err := triage.StoreDecision(m.Subject, m.From, m.Date, m.Body, isImportant); err != nil {
slog.Warn("[Triage-Learn] Speichern fehlgeschlagen", "betreff", m.Subject, "fehler", err)
continue
}
if isImportant {
wichtig++
} else {
unwichtig++
}
}
slog.Info("[Triage-Learn] Ordner verarbeitet",
"account", accountLabel(acc),
"ordner", af.IMAPFolder,
"emails", len(msgs),
"wichtig", isImportant,
)
}
return wichtig, unwichtig, nil
}
// LearnFromFoldersAllAccounts führt LearnFromFolders für alle konfigurierten Accounts aus.
func LearnFromFoldersAllAccounts() (wichtig, unwichtig int, err error) {
accounts := config.AllEmailAccounts()
for _, acc := range accounts {
w, u, accErr := LearnFromFolders(acc)
if accErr != nil {
slog.Error("[Triage-Learn] Account fehlgeschlagen", "account", accountLabel(acc), "fehler", accErr)
continue
}
wichtig += w
unwichtig += u
}
return wichtig, unwichtig, nil
}