feat: LLM-gestützte Mail-Klassifizierung (Wichtig/Aktion/Newsletter/Spam)

This commit is contained in:
root 2026-03-09 15:29:43 +07:00
parent fdf2bc095a
commit eb34f354b3
4 changed files with 162 additions and 1 deletions

View file

@ -221,6 +221,14 @@ def _tool_get_todays_mails() -> str:
return "\n".join(lines) return "\n".join(lines)
def _tool_get_smart_mail_digest(hours: int = 24) -> str:
cfg = _load_config()
mail_client.init(cfg)
api_key = cfg.api_keys.get("openrouter_key", "")
digest = mail_client.get_smart_digest(hours=hours, api_key=api_key)
return mail_client.format_smart_digest(digest)
def _tool_get_feed_stats() -> str: def _tool_get_feed_stats() -> str:
cfg = _load_config() cfg = _load_config()
ct_109 = config.get_container(cfg, vmid=109) ct_109 = config.get_container(cfg, vmid=109)
@ -262,4 +270,5 @@ def get_tool_handlers() -> dict:
"get_mail_count": lambda: _tool_get_mail_count(), "get_mail_count": lambda: _tool_get_mail_count(),
"search_mail": lambda query, days=30: _tool_search_mail(query, days=days), "search_mail": lambda query, days=30: _tool_search_mail(query, days=days),
"get_todays_mails": lambda: _tool_get_todays_mails(), "get_todays_mails": lambda: _tool_get_todays_mails(),
"get_smart_mail_digest": lambda hours=24: _tool_get_smart_mail_digest(hours=hours),
} }

View file

@ -1,7 +1,13 @@
"""IMAP Mail Client — Liest E-Mails vom All-Inkl Spiegel-Postfach (Read-Only).""" """IMAP Mail Client — Liest E-Mails vom All-Inkl Spiegel-Postfach (Read-Only).
Stufe 1: Keyword-Filter (IMPORTANT_SENDERS)
Stufe 2: LLM-Klassifizierung (classify_mails) trennt Spam/Newsletter von Wichtigem.
"""
import imaplib import imaplib
import email import email
import json
import requests as _req
from email.header import decode_header from email.header import decode_header
from email.utils import parsedate_to_datetime from email.utils import parsedate_to_datetime
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
@ -204,6 +210,138 @@ def get_important_mails(hours: int = 24) -> list[dict]:
m.logout() m.logout()
CLASSIFY_PROMPT = """Du bekommst eine Liste von E-Mails (Absender + Betreff).
Klassifiziere JEDE Mail in genau eine Kategorie:
- "wichtig": Rechnungen, Sicherheitswarnungen, Server-Alerts, Bank, Behörden, persönliche Nachrichten
- "aktion": Erfordert eine Handlung (Passwort ändern, Zahlung fällig, Termin bestätigen)
- "info": Nützliche Info aber keine Handlung nötig (Versandbestätigung, Status-Update)
- "newsletter": Newsletter, Marketing, Angebote, Werbung
- "spam": Offensichtlicher Spam, Phishing, unseriös
Antworte NUR mit einem JSON-Array. Pro Mail ein Objekt mit "idx" (0-basiert) und "cat" (Kategorie).
Beispiel: [{"idx":0,"cat":"newsletter"},{"idx":1,"cat":"wichtig"}]"""
def classify_mails(mails: list[dict], api_key: str) -> list[dict]:
"""LLM-gestützte Klassifizierung von Mails nach Wichtigkeit."""
if not mails or not api_key:
return mails
mail_text = "\n".join(
f"{i}. Von: {m['from'][:50]} | Betreff: {m['subject'][:80]}"
for i, m in enumerate(mails)
)
try:
r = _req.post(
"https://openrouter.ai/api/v1/chat/completions",
headers={"Authorization": f"Bearer {api_key}"},
json={
"model": "openai/gpt-4o-mini",
"messages": [
{"role": "system", "content": CLASSIFY_PROMPT},
{"role": "user", "content": mail_text},
],
"max_tokens": 400,
"temperature": 0,
},
timeout=30,
)
r.raise_for_status()
content = r.json()["choices"][0]["message"]["content"]
content = content.strip()
if content.startswith("```"):
content = content.split("\n", 1)[-1].rsplit("```", 1)[0]
classifications = json.loads(content)
cat_map = {c["idx"]: c["cat"] for c in classifications}
for i, m in enumerate(mails):
m["category"] = cat_map.get(i, "unknown")
return mails
except Exception:
for m in mails:
m["category"] = "unknown"
return mails
def get_smart_digest(hours: int = 24, api_key: str = "") -> dict:
"""Intelligente Mail-Zusammenfassung: holt Mails, klassifiziert per LLM, gruppiert."""
m = _connect()
if not m:
return {"error": "IMAP-Verbindung fehlgeschlagen"}
try:
m.select("INBOX", readonly=True)
since = (datetime.now() - timedelta(hours=hours)).strftime("%d-%b-%Y")
_, data = m.search(None, f'(SINCE "{since}")')
ids = data[0].split() if data[0] else []
mails = []
for mid in ids[-50:]:
_, msg_data = m.fetch(mid, "(BODY.PEEK[HEADER])")
parsed = _parse_mail(msg_data)
if parsed:
mails.append(parsed)
mails.reverse()
except Exception as e:
return {"error": str(e)}
finally:
m.logout()
if not mails:
return {"total": 0, "mails": [], "summary": {}}
if api_key:
mails = classify_mails(mails, api_key)
summary = {}
for m_item in mails:
cat = m_item.get("category", "unknown")
summary.setdefault(cat, []).append(m_item)
return {"total": len(mails), "mails": mails, "summary": summary}
def format_smart_digest(digest: dict) -> str:
"""Formatiert den intelligenten Digest für Telegram."""
if "error" in digest:
return f"Mail-Fehler: {digest['error']}"
if digest.get("total", 0) == 0:
return "Keine neuen Mails im gewählten Zeitraum."
lines = [f"📧 Mail-Digest ({digest['total']} Mails)\n"]
summary = digest.get("summary", {})
cat_labels = {
"wichtig": "🔴 Wichtig",
"aktion": "⚡ Aktion nötig",
"info": " Info",
"newsletter": "📰 Newsletter",
"spam": "🗑️ Spam",
"unknown": "❓ Unkategorisiert",
}
cat_order = ["aktion", "wichtig", "info", "newsletter", "spam", "unknown"]
for cat in cat_order:
cat_mails = summary.get(cat, [])
if not cat_mails:
continue
label = cat_labels.get(cat, cat)
lines.append(f"{label} ({len(cat_mails)}):")
show = cat_mails if cat in ("wichtig", "aktion", "info") else cat_mails[:3]
for m_item in show:
lines.append(f" {m_item['date_str']} | {m_item['from'][:30]}")
lines.append(f"{m_item['subject'][:65]}")
if len(cat_mails) > len(show):
lines.append(f" ... und {len(cat_mails) - len(show)} weitere")
lines.append("")
wichtig = len(summary.get("wichtig", [])) + len(summary.get("aktion", []))
noise = len(summary.get("newsletter", [])) + len(summary.get("spam", []))
lines.append(f"Fazit: {wichtig} relevante, {noise} ignorierbare Mails")
return "\n".join(lines)
def format_summary() -> str: def format_summary() -> str:
"""Komplett-Übersicht: Counts + letzte Mails + wichtige.""" """Komplett-Übersicht: Counts + letzte Mails + wichtige."""
counts = get_mail_count() counts = get_mail_count()

View file

@ -214,6 +214,20 @@ TOOLS = [
"parameters": {"type": "object", "properties": {}, "required": []}, "parameters": {"type": "object", "properties": {}, "required": []},
}, },
}, },
{
"type": "function",
"function": {
"name": "get_smart_mail_digest",
"description": "Intelligente Mail-Zusammenfassung: KI klassifiziert Mails in Wichtig/Aktion/Info/Newsletter/Spam. Nutze dies wenn der User nach 'wichtigen Mails' fragt oder wissen will ob etwas Relevantes dabei ist.",
"parameters": {
"type": "object",
"properties": {
"hours": {"type": "integer", "description": "Zeitraum in Stunden (default: 24)", "default": 24},
},
"required": [],
},
},
},
] ]