docs/novaberg-ei-dual-emotion_t.md
Rama master · réplica de lectura del repositorio original.
# Novaberg — Dual-Emotion Phase 2: Novas Emotionsstrang (Ausarbeitung)
**Teil 2 von 5 — Ausarbeitung.** Absicht und Kopfblock: [`novaberg-ei-dual-emotion_k.md`](novaberg-ei-dual-emotion_k.md) · Bauplan und Umstellung: [`novaberg-ei-dual-emotion_b.md`](novaberg-ei-dual-emotion_b.md) · Diskussion und Ergänzungen: [`novaberg-ei-dual-emotion_e.md`](novaberg-ei-dual-emotion_e.md). Die Abschnittsnummern sind die des ungeteilten Konzepts; welche Datei einen Abschnitt trägt, sagt die Tabelle *„§ → Datei“* in `_k`.
---
## 2. Der Kreislauf
```
Turn N:
[Synchron — User wartet]
Perzeption(User) → Emotion, Arousal, Modus, Intent des Users
Enricher(laden) → Session, KZG, LZG, Hash laden (User + Nova)
EI-Calc(User) → Emotions-Verlauf, Vektor, Stil, Plausibilitäts-Gate
Nova-Emotion(N-1) laden → Decay + Empathie
→ Nova-Emotion(N) berechnen → in State
Router → Routing-Entscheidungen (sieht jetzt volle Session + EI)
[Planner → Agent] → Bei Management-Aktion
GV-Node → Gesprächsvektor
Responder → Antwort generieren (mit Nova-Emotion als Kontext)
Thinker → Faktenprüfung
Tribunal → Qualitätskontrolle
[Corrector] → Bei Ablehnung
[Antwort an User]
[Asynchron — User wartet nicht]
Nova (voller Pfad, ASSISTANT_USER_ID):
Perzeption(Nova) → Emotion, Arousal, Modus, Intent von Novas Antwort
Enricher(Nova) → Novas Session-Turns laden (ASSISTANT_USER_ID)
EI-Calc(Nova) → Nova-Emotions-Verlauf + Vektor berechnen
Router(Nova) → Prüft auf Commitments (Agent-Bedarf)
[Planner → Agent] → Bei Commitment (selten, z.B. "Ich erinnere dich morgen")
Salienz(Nova) → Nova-Antwort bewerten → pending_writes(Nova)
Dispatcher(Nova) → Schreibt unter ASSISTANT_USER_ID
User (nur speichern, Daten liegen bereits im State):
Salienz(User) → User-Prompt bewerten → pending_writes(User)
Dispatcher(User) → Schreibt unter DEFAULT_USER_ID
```
---
## 3. Enricher-Split: Laden vs. Rechnen
### 3.1 Problem
Der Enricher ist ~1000 Zeilen und mischt zwei grundverschiedene Verantwortungen: Daten laden (I/O) und EI berechnen (reine Funktionen). Für Phase 2 müssen die EI-Berechnungen wiederverwendbar sein — einmal für den User (synchron), einmal für Nova (asynchron).
### 3.2 Split
**`ei/berechnung.py`** — ~600 Zeilen, reine Funktionen, null I/O:
| Funktion | Aufgabe |
|----------|---------|
| `_emotion_kanonisieren()` | Synonym-Auflösung |
| `_arousal_to_float()` | Legacy-Konvertierung |
| `_emotions_verlauf_berechnen()` | Gewichteter Verlauf mit Decay + Plutchik-Normalisierung |
| `_emotion_zu_gruppe()` | Sektor → Gruppe |
| `_dominante_gruppe()` | Dominante Gruppe einer Turn-Liste |
| `_emotions_vektor_bestimmen()` | 9 Richtungsvektoren |
| `_turn_features_bewerten()` | 13 Stilmerkmale pro Turn |
| `_hash_stil_extrahieren()` | Stil aus Charakter-Hash |
| `_sprach_stil_erkennen()` | Akkumuliertes Feature-Scoring |
| `_ei_arousal_berechnen()` | Gewichteter EI-Faktor |
| `_modus_plausibilitaet()` | Matrix-Lookup Modus |
| `_stil_plausibilitaet()` | Stil-Gegencheck |
**`graph/nodes/enricher.py`** — ~400 Zeilen, nur Daten laden:
| Block | Aufgabe |
|-------|---------|
| Session-Turns aus Redis | `session_turns_retrieve()`, Shadow-Filter |
| Plugin-Hooks | `manager.enrich()` Loop |
| Charakter-Anweisungen + Direktiven | PostgreSQL SELECT |
| KZG/LZG Suche | Embedding erzeugen, Vektor-Suche |
| Charakter-Hash (User + Nova) | `charakter_hash_retrieve_dict()` |
### 3.3 EI-Calc als eigener Node
**Neuer Node:** `graph/nodes/ei_calc.py`
Importiert die Funktionen aus `ei/berechnung.py`, ruft sie mit den geladenen Daten aus dem State auf, schreibt die Ergebnisse zurück in den State.
**Kein LLM-Call.** Reine Python-Berechnung, deterministisch, < 100ms.
**Position im Graph:** Nach Enricher(laden), vor Router. Damit sieht der Router die vollständigen EI-Ergebnisse und kann bessere Routing-Entscheidungen treffen (löst ROUTE-MISS1).
**Dual-Modus:** Im synchronen Pfad berechnet EI-Calc die User-EI UND lädt Novas Emotions-Historie für die Nova-Emotion(N)-Berechnung (Decay + Empathie). Im asynchronen Pfad berechnet derselbe Node Novas EI aus der frischen Perzeption.
---
## 4. Nova-Emotion: Quellen und Berechnung
Pro Turn wirken zwei Kräfte auf Novas Position im 8-dimensionalen Plutchik-Raum (Phase 3 fügt die dritte hinzu):
### 4.1 Vorheriger Zustand × Decay
Novas Emotionsvektor tendiert zur Neutralität zurück, wenn nichts ihn verstärkt. Dieselbe Decay-Mechanik wie beim User — `_emotions_verlauf_berechnen()` auf Novas Turn-Historie.
### 4.2 Nutzer-Vektor × α (Asymmetrische Empathie)
Die Emotion des Users wirkt als Kraft auf Novas Position. Der Empathie-Faktor α hängt von der Sektor-Distanz im Plutchik-Oktagon ab:
| Sektor-Distanz | α-Bereich | Beispiel |
|----------------|-----------|---------|
| 0–1 (benachbart) | 0.1–0.2 | Beide freudig → Bestätigung |
| 2 (nah-diagonal) | 0.3–0.4 | Nova neugierig, User überrascht → leichte Modulation |
| 3–4 (gegenüber) | 0.7–0.9 | Nova freudig, User traurig → Empathie überschreibt |
Die Sektor-Distanzmatrix existiert bereits (`EMOTION_SEKTOR_DISTANZ` in `config.py`).
> **Abgrenzung zum Raumzug (Chat 114) — das Vorzeichen kehrt sich um.** Novas Register hat seit Chat 114 dieselbe Bauart: ein eigener Zustand, der vom Nutzer angezogen wird (`ei/raum.py`, Konzept in `novaberg-gv-strategie_k.md` §3.4). Die Formel ist dieselbe Gestalt, die Abhängigkeit von der Distanz aber die **entgegengesetzte**.
>
> | | Emotion | Register |
> |---|---|---|
> | Große Distanz | zieht **stärker** — Empathie überschreibt | zieht **nicht** stärker |
> | Richtung | symmetrisch | hinauf (System 1 → System 2) langsamer als hinab |
> | Faktor aus | Sektor-Distanz im Plutchik-Oktagon | fester Zug × Charakterfaktor |
>
> Der Grund ist inhaltlich: Ein Gegenüber, das emotional weit weg steht, zieht mich mit — das ist Mitgefühl. Ein Gegenüber, das das Register wechselt, zieht mich nicht schneller, weil das gedankliche Umstellen Zeit kostet, egal wie weit der Sprung ist.
>
> **Wer die α-Tabelle oben fürs Register abschreibt, baut das Gegenteil dessen, was entschieden wurde.**
### 4.3 Ziel-Vektor × Similarity (Phase 3 — noch nicht)
Aktivierte Zielsätze injizieren Emotion. Kommt mit dem Antrieb-System (novaberg-thinking-drive_k.md §4.3). In Phase 2 hat Nova zwei Quellen, keine drei.
### 4.4 Konflikterkennung
Wenn Empathie-Vektor und Novas eigener Zustand in entgegengesetzte Richtungen zeigen (Cosine-Similarity < -0.3), wird ein `nova_emotion_konflikt`-Flag gesetzt. Der Responder kann den Konflikt aktiv ausdrücken: "Ich freue mich für dich, aber ich mache mir Sorgen."
---
## 5. Datentrennung
Strikte Partition über `user_id`:
| Daten | Partition | Beispiel |
|-------|-----------|---------|
| User-Emotionen | `DEFAULT_USER_ID` | "meister" |
| User-KZG/LZG | `DEFAULT_USER_ID` | "meister" |
| Nova-Emotionen | `ASSISTANT_USER_ID` | "nova" |
| Nova-KZG | `ASSISTANT_USER_ID` | "nova" |
| Nova-Charakter-Hash | `ASSISTANT_USER_ID` | "nova" |
**Umschalten auf Charlotte** → anderer `ASSISTANT_USER_ID` → komplett andere Emotionshistorie, anderes KZG, anderer Charakter. Dasselbe gilt umgekehrt: anderer User → anderer `DEFAULT_USER_ID`. `[Herkunft geprüft 19.09.2026: keine realen Personen oder Angaben]`
**Innerhalb eines Turns** fließt alles durch den State — das ist das eine Momentum. User-Daten und Nova-Daten koexistieren im selben State-Dict, werden aber getrennt gespeichert.
---
## 6. Asynchroner Block — Details
> **⚠️ Veraltet (Chat 60).** Der hier beschriebene asynchrone Block wurde durch das Event-Modell ersetzt. Der CharacterGraph (Pfad 2) läuft als eigenständiger Graph, ausgelöst durch Events. Siehe `novaberg-convention-event-model.md`.
### 6.1 Perzeption(Nova)
Derselbe Perzeption-Node, aufgerufen mit Novas `response` statt `user_prompt`. Der Prompt wird minimal angepasst: "Analysiere die folgende Aussage der Assistentin" statt "Analysiere den Prompt des Nutzers."
Extrahiert: `nova_response_emotion`, `nova_response_arousal`, `nova_response_modus`, `nova_response_intent`, `nova_response_tone`, `nova_response_beziehungs_dynamik`.
### 6.2 Enricher(Nova)
Derselbe Enricher-Node, aber mit `ASSISTANT_USER_ID`. Lädt Novas eigene Session-Turns, ihr KZG, ihr LZG, ihren Charakter-Hash. Der User-Enricher lief synchron — jetzt läuft Novas Enricher asynchron auf ihrer eigenen Partition.
### 6.3 EI-Calc(Nova)
Gehört zur Perzeption — erst wahrnehmen, dann berechnen. Dieselben Funktionen wie EI-Calc(User), aber auf Novas Turn-Historie (`ASSISTANT_USER_ID`-Partition):
- `_emotions_verlauf_berechnen()` auf Novas Session-Turns
- `_emotions_vektor_bestimmen()` auf Novas Session-Turns
- Empathie-Berechnung: Novas neuer Vektor unter Einfluss des User-Vektors
Schreibt: `nova_emotions_verlauf` in den State; den Vektor nach `internal.emotion.emotions_vector` (Personality-Klasse — ein flacher State-Key `nova_emotions_vektor` existiert seit dem Personality-Umbau nicht mehr; der Responder liest seit Chat 106 direkt aus der Klasse, RESPONDER-VEKTOR-TOT).
### 6.4 Router(Nova)
Derselbe Router-Node, prüft Novas Antwort auf Commitments. In den meisten Fällen: kein Agent-Bedarf → weiter zur Salienz. Im seltenen Fall, dass Nova ein Versprechen gemacht hat ("Ich erinnere dich morgen") → Planner → TimelineAgent/WiedervorlageAgent.
### 6.5 Salienz + Dispatcher
Zwei getrennte Pfade:
**Nova-Pfad (im selben async-Block):**
Salienz(Nova) → "Hat die Assistentin etwas gesagt, das sie sich merken sollte?" → `pending_writes` mit `user_id = ASSISTANT_USER_ID` → Dispatcher(Nova) schreibt unter `ASSISTANT_USER_ID`.
**User-Pfad (parallel oder sequentiell):**
Salienz(User) → "Hat der User etwas Wichtiges gesagt?" → `pending_writes` mit `user_id = DEFAULT_USER_ID` → Dispatcher(User) schreibt unter `DEFAULT_USER_ID`. Keine Enricher/EI-Calc nötig — die Daten liegen bereits im State vom synchronen Durchlauf.
### 6.6 Laufzeit-Schätzung
| Schritt | GPU-Calls | Geschätzte Zeit |
|---------|-----------|----------------|
| **Nova-Pfad:** | | |
| Perzeption(Nova) | 1 | ~1-2s |
| Enricher(Nova) | 0 (I/O) | <0.5s |
| EI-Calc(Nova) | 0 (Python) | <0.1s |
| Router(Nova) | 1 | ~1-2s |
| Salienz(Nova) | 1 | ~2-3s |
| Dispatcher(Nova) | 0 (Python) | <0.1s |
| **User-Pfad:** | | |
| Salienz(User) | 1 | ~2-3s |
| Dispatcher(User) | 0 (Python) | <0.1s |
| **Gesamt (ohne Agent)** | **4** | **~6-10s** |
| + Agent bei Commitment | +1-2 | ~9-15s |
Alles asynchron — der User sieht die Antwort sofort nach dem Tribunal.
---
## 7. Neue State-Felder
| Feld | Typ | Quelle | Beschreibung |
|------|-----|--------|-------------|
| `nova_response_emotion` | `str` | Perzeption(Nova) | Novas dominante Emotion in dieser Antwort |
| `nova_response_arousal` | `float` | Perzeption(Nova) | Novas Arousal in dieser Antwort |
| `nova_response_modus` | `str` | Perzeption(Nova) | Novas Kommunikationsregister |
| `nova_response_intent` | `str` | Perzeption(Nova) | Novas Kommunikationsabsicht |
| `nova_emotions_verlauf` | `list[dict]` | EI-Calc(Nova) | Gewichteter Verlauf mit Decay |
| `internal.emotion.emotions_vector` | `str` | EI-Calc(Nova) | Richtungsvektor (9 Werte) — Klassen-Feld, kein flacher Key (Chat 106) |
| `nova_emotion_konflikt` | `bool` | EI-Calc | Empathie vs. eigener Zustand |
---
> **Hinweis zur Aufteilung (19.09.2026):** §8 *Graph-Änderungen* steht in [`novaberg-ei-dual-emotion_b.md`](novaberg-ei-dual-emotion_b.md).
---
## 9. Responder-Integration ✅
Der Responder bekommt Novas berechnete Emotion für den aktuellen Turn als zusätzlichen Kontext im `[EIGENE_EMOTION]`-Block:
```
[EIGENE_EMOTION]
Dein aktueller emotionaler Zustand: {nova_emotion_label}
Arousal: {nova_arousal}
Vektor: {nova_emotions_vektor}
```
Platziert zwischen [IDENTITAET] und [KOMMUNIKATION]. Die Emotion beeinflusst die Antwort, ohne sie zu diktieren — Nova kann fröhlich sein und trotzdem sachlich antworten, aber die Grundfärbung ändert sich.
**Status:** Implementiert. Der Block wird aus `nova_emotions_verlauf`, `nova_emotions_vektor` und `nova_emotion_konflikt` (State-Felder aus EI-Calc) zusammengebaut.
---