# Email Ingestion Architecture — Hermes Agent
**Version:** 1.0
**Status:** Verbindlich
**Erstellt:** 2026-05-21
**Gilt für:** hermes-carlo
**Abhängigkeiten:** Tool-Gateway (8645), Session-Memory, Review-Queue
---
## Grundsatz
E-Mails sind Roh-Eingabe — keine Wahrheit.
Original-E-Mails werden niemals verändert.
Kein direkter Obsidian- oder Graphiti-Write.
Jede extrahierte Information wird als `proposed_fact` behandelt.
---
## Pipeline-Übersicht
```
EINGANG (E-Mail)
│
▼
[1] RAW SPEICHERN → /ingestion/email/raw/{sha256}.json
│ Original unverändert, SHA256 für Duplikatsprüfung
▼
[2] DUPLIKAT-CHECK → SHA256 gegen raw/ prüfen
│ Duplikat → /ingestion/email/rejected/{sha256}_duplicate.json
▼
[3] TEXT EXTRAHIEREN → /ingestion/email/extracted/{sha256}.txt
│ Subject + Body, HTML→Text, Attachments ignoriert (Phase 1)
▼
[4] PII / SECRET MASKING → /ingestion/email/masked/{sha256}.txt
│ E-Mail, Telefon, IBAN, API-Keys, Passwörter
▼
[5] PROJEKT ERKENNEN → /ingestion/email/classified/{sha256}.json
│ Keyword-Matching gegen PROJECT_MAP
▼
[6] RETRIEVAL → POST /retrieval/context
│ Kontext aus Obsidian + Graphiti laden
▼
[7] PROPOSED_FACTS ERZEUGEN
│ Aus E-Mail-Inhalt + Retrieval-Kontext ableiten
│ Status immer: inferred (nicht user_stated, nicht verified)
▼
[8] REVIEW-QUEUE → /memory/review-queue/{review_id}.json
│ Ein Eintrag pro proposed_fact
▼
[9] AUDIT LOG → /ingestion/email/ingestion_audit.jsonl
```
---
## Verzeichnisstruktur
```
/opt/struktur/ingestion/email/
├── raw/ ← Original-E-Mails (niemals verändern)
│ └── {sha256}.json
├── extracted/ ← Reiner Text (Subject + Body)
│ └── {sha256}.txt
├── classified/ ← Projektzuordnung + Metadaten
│ └── {sha256}.json
├── masked/ ← PII-maskierter Text
│ └── {sha256}.txt
├── rejected/ ← Dubletten + fehlerhafte Eingaben
│ └── {sha256}_{reason}.json
├── approved/ ← Nach Human-Review approved (leer in Phase 1)
└── ingestion_audit.jsonl ← Jede Operation geloggt
```
---
## Schritt 1: Raw-Speicherung
### Format (`raw/{sha256}.json`):
```json
{
"sha256": "abc123...",
"ingested_at": "2026-05-21T14:00:00Z",
"source": "manual | gmail_api | imap",
"message_id": "<id@domain>",
"from_raw": "[EMAIL]",
"to_raw": "[EMAIL]",
"subject": "...",
"date": "2026-05-21T13:00:00Z",
"body_raw": "...",
"attachments": [],
"processing_status": "raw"
}
```
### Regeln:
- SHA256 über `message_id + subject + date` (nicht über Body — zu volatil)
- `from_raw` und `to_raw` sofort als `[EMAIL]` maskieren beim Speichern
- Original-Body unverändert in `body_raw`
- `processing_status` wird durch Pipeline-Schritte aktualisiert
---
## Schritt 2: Duplikat-Check
### Methode:
SHA256 des eingehenden Mails gegen alle Dateien in `raw/` prüfen.
```python
sha256 = hashlib.sha256(f"{message_id}{subject}{date}".encode()).hexdigest()
if (RAW_DIR / f"{sha256}.json").exists():
→ rejected/{sha256}_duplicate.json
```
### Rejected-Format:
```json
{
"sha256": "...",
"reason": "duplicate",
"rejected_at": "...",
"original_file": "raw/{sha256}.json"
}
```
---
## Schritt 3: Textextraktion
### Was extrahiert wird:
- Subject (vollständig)
- Body (plain text; HTML wird zu Text konvertiert)
### Was ignoriert wird (Phase 1):
- Anhänge (PDFs, Bilder, Excel)
- Inline-Bilder
- E-Mail-Header (außer Subject/Date/From/To)
### Output (`extracted/{sha256}.txt`):
```
SUBJECT: [Betreff]
DATE: [Datum]
---
[Body als plain text]
```
---
## Schritt 4: PII- und Secret-Masking
### Maskiert werden:
| Typ | Muster | Ersatz |
|-----|--------|--------|
| API-Keys | `sk-...`, `ghp_...`, `sk-or-v1-...` | `[API_KEY]` |
| Tokens | `Bearer ...`, `xoxb-...` | `[TOKEN]` |
| Passwörter | `password=...`, `pwd:...` | `[PASSWORD]` |
| E-Mail-Adressen | `x@y.z` | `[EMAIL]` |
| Telefonnummern | `+34 ...`, `0049...` | `[PHONE]` |
| IBAN | `DE12 ...` | `[IBAN]` |
### Wichtig:
- Masking läuft auf dem maskierten Text — nicht auf dem Raw
- Raw-Datei bleibt immer unverändert
- Masking-Output nach `masked/{sha256}.txt`
---
## Schritt 5: Projekterkennung
### Methode: Keyword-Matching
```python
PROJECT_MAP = {
"vento": ["vento", "whatsapp", "dialog-engine", "twilio"],
"hermes": ["hermes", "agent", "gateway", "retrieval"],
"graphiti": ["graphiti", "knowledge graph", "neo4j"],
"mirofish": ["mirofish", "multi-agent", "simulation"],
"n8n": ["n8n", "workflow", "automation"],
"the donna": ["the donna", "donna", "electron"],
"kiki": ["kiki", "lernbegleiterin"],
"agent solutions": ["agent solutions", "agentsolutions"],
}
```
### Scoring:
- Jeder Treffer im Subject: 3 Punkte
- Jeder Treffer im Body: 1 Punkt
- Projekt mit höchstem Score gewinnt
- Kein klares Ergebnis (Score = 0): `project = "unclassified"`
### Output (`classified/{sha256}.json`):
```json
{
"sha256": "...",
"project": "vento",
"project_score": 7,
"matched_keywords": ["vento", "whatsapp"],
"classified_at": "..."
}
```
---
## Schritt 6: Retrieval
### Aufruf:
```json
POST /retrieval/context
{
"query": "<Subject der E-Mail>",
"project": "<erkanntes Projekt>"
}
```
### Zweck:
- Bestehenden Obsidian-Kontext laden
- Graphiti-Facts laden
- Abgleich: Widerspricht der E-Mail-Inhalt bestehendem Wissen?
- Confidence bestimmt Fact-Status der proposed_facts
### Confidence-Mapping:
| Retrieval-Confidence | proposed_fact Status |
|---------------------|---------------------|
| `high` | `inferred` (guter Kontext vorhanden) |
| `medium` | `inferred` |
| `low` | `inferred` + Risk-Flag "Wenig Kontext" |
---
## Schritt 7: Proposed Facts erzeugen
### Regeln:
- Status immer `inferred` (nie `user_stated`, nie `verified`)
- E-Mail ist Fremdquelle — nicht vertrauenswürdiger als Chat
- Maximal 5 proposed_facts pro E-Mail
- Nur faktische Aussagen, keine Vermutungen extrahieren
- Kein PII in proposed_facts
### Format:
```json
{
"fact": "Vento Telegram-Integration wurde implementiert",
"status": "inferred",
"confidence": "unverified",
"source_type": "email",
"source_sha256": "abc123...",
"proposed_obsidian_page": "03-Projekte/LP-VENTO.md",
"obsidian_write_allowed": false,
"graphiti_write_allowed": false
}
```
---
## Schritt 8: Review-Queue-Integration
### Pro proposed_fact eine Review-Datei:
```json
{
"review_id": "REVIEW-{ts}-{uuid6}",
"created_at": "...",
"status": "pending",
"source_type": "email",
"source_sha256": "abc123...",
"source_subject": "[maskierter Betreff]",
"proposed_fact": "...",
"fact_status": "inferred",
"confidence": "unverified",
"requires_human_review": true,
"obsidian_write_allowed": false,
"graphiti_write_allowed": false,
"expires_at": "<created_at + 14 Tage>"
}
```
---
## Schritt 9: Audit-Logging
### Jede Operation wird in `ingestion_audit.jsonl` geloggt:
```json
{"timestamp": "...", "sha256": "...", "step": "raw|extract|mask|classify|retrieval|facts|review", "status": "ok|error|duplicate", "detail": "..."}
```
---
## Fehlerbehandlung
| Fehler | Verhalten |
|--------|-----------|
| Duplikat erkannt | → `rejected/`, Pipeline stoppt, Audit-Eintrag |
| Textextraktion schlägt fehl | → Pipeline stoppt, Fehler in Audit |
| PII-Masking schlägt fehl | → Pipeline stoppt, Raw-Datei bleibt unverändert |
| Projekterkennung: kein Treffer | → `project: unclassified`, Pipeline läuft weiter |
| Retrieval nicht erreichbar | → proposed_facts trotzdem erzeugen, Risk-Flag: "Kein Retrieval-Kontext" |
| Review-Queue-Write schlägt fehl | → Fehler in Audit, proposed_facts gehen verloren (kein Silent Fail) |
---
## Sicherheitsregeln
| Regel | Detail |
|-------|--------|
| Original niemals verändern | `raw/` ist write-once |
| Secrets niemals persistieren | Masking vor jedem Write außer `raw/` |
| Kein Obsidian-Write | Kein Endpunkt freigegeben |
| Kein Graphiti-Write | Kein Endpunkt freigegeben |
| Kein Silent Fail | Jeder Fehler wird auditiert |
| Kein Batch-Auto-Approve | Jeder proposed_fact einzeln per Human-Review |
---
## Erkannte Risiken
| Risiko | Impact | Mitigation |
|--------|--------|-----------|
| E-Mail enthält API-Key im Body | Kritisch | Masking vor Extract; Raw nie geteilt |
| Falsche Projektzuordnung | Mittel | Score-Threshold; unclassified als Fallback |
| Viele proposed_facts aus Sammel-Mails | Mittel | Max. 5 Facts pro E-Mail |
| Retrieval offline → Facts ohne Kontext | Mittel | Risk-Flag, Human entscheidet bei Review |
| Gleiche Information aus mehreren Quellen | Niedrig | SHA256-Duplikatsprüfung + Dublettencheck in Review |
| PII in proposed_fact | Hoch | Masking auf maskierten Text anwenden bevor Facts extrahiert werden |
---
*Architekturdatei: `/opt/struktur/hermes-carlo/ingestion/EMAIL_INGESTION_ARCHITECTURE.md`*
*Pipeline-Script: `/opt/struktur/ingestion/ingest_email.py`*
*Ingestion-Root: `/opt/struktur/ingestion/email/`*