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

@@ -210,6 +210,30 @@ var (
Name: "triage",
Description: "Letzte 10 Emails klassifizieren und in Wichtig/Unwichtig verschieben",
},
{
Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "triage-history",
Description: "Letzte Triage-Entscheidungen anzeigen",
Options: []*discordgo.ApplicationCommandOption{
{Type: discordgo.ApplicationCommandOptionInteger, Name: "anzahl", Description: "Anzahl (Standard: 10)", Required: false},
},
},
{
Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "triage-correct",
Description: "Triage-Entscheidung korrigieren (wichtig↔unwichtig umkehren)",
Options: []*discordgo.ApplicationCommandOption{
{Type: discordgo.ApplicationCommandOptionString, Name: "betreff", Description: "Email-Betreff (Teilstring reicht)", Required: true},
},
},
{
Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "triage-search",
Description: "Ähnliche Triage-Entscheidungen suchen",
Options: []*discordgo.ApplicationCommandOption{
{Type: discordgo.ApplicationCommandOptionString, Name: "query", Description: "Suchbegriff", Required: true},
},
},
{
Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "move",
@@ -570,8 +594,17 @@ func handleEmailCommand(s *discordgo.Session, i *discordgo.InteractionCreate) {
}
args := []string{sub.Name}
if sub.Name == agents.ActionEmailIngest && len(sub.Options) > 0 {
args = append(args, sub.Options[0].StringValue())
if len(sub.Options) > 0 {
switch sub.Name {
case agents.ActionEmailIngest:
args = append(args, sub.Options[0].StringValue())
case agents.ActionEmailTriageHistory:
args = append(args, fmt.Sprintf("%d", sub.Options[0].IntValue()))
case agents.ActionEmailTriageCorrect:
args = append(args, sub.Options[0].StringValue())
case agents.ActionEmailTriageSearch:
args = append(args, sub.Options[0].StringValue())
}
}
handleAgentResponse(s, i, func() agents.Response {
return toolAgent.Handle(agents.Request{Action: agents.ActionEmail, Args: args})
@@ -1215,14 +1248,34 @@ func nightlyIngest(channelID string) {
}
}
// Triage-Lernen: Entscheidungen aus Ordnerstruktur ableiten
wichtig, unwichtig, learnErr := email.LearnFromFoldersAllAccounts()
if learnErr != nil {
slog.Error("Triage-Lernen Fehler", "fehler", learnErr)
} else if wichtig+unwichtig > 0 {
slog.Info("Triage-Lernen abgeschlossen", "wichtig", wichtig, "unwichtig", unwichtig)
}
if channelID == "" {
return
}
msg := ""
if len(errs) > 0 {
dg.ChannelMessageSend(channelID, fmt.Sprintf("⚠️ Nacht-Ingest: %d Emails importiert, %d Fehler:\n%s",
total, len(errs), strings.Join(errs, "\n")))
msg = fmt.Sprintf("⚠️ Nacht-Ingest: %d Emails importiert, %d Fehler:\n%s",
total, len(errs), strings.Join(errs, "\n"))
} else if total > 0 {
dg.ChannelMessageSend(channelID, fmt.Sprintf("🗄️ Nacht-Ingest: %d Emails aus Archiv-Ordnern importiert.", total))
msg = fmt.Sprintf("🗄️ Nacht-Ingest: %d Emails aus Archiv-Ordnern importiert.", total)
}
if wichtig+unwichtig > 0 {
learnMsg := fmt.Sprintf("🧠 Triage-Lernen: %d wichtig, %d unwichtig gelernt.", wichtig, unwichtig)
if msg != "" {
msg += "\n" + learnMsg
} else {
msg = learnMsg
}
}
if msg != "" {
dg.ChannelMessageSend(channelID, msg)
}
}