Explorer
/opt/obsidian-vault/Runbooks/EMAIL_INGESTION_ARCHITECTURE.md
← Zurück ↓ Download
# 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/`*