Ctrl K
ComunidadGuías…

docs/novaberg-mem-knowledge-graph.md

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Novaberg — Gedaechtnis: Knowledge Graph

**Projekt:** Novaberg — The Nova Anima Resonance System
**Dokument:** Knowledge Graph — Entitaeten, Fakten, Entity Resolution, Bi-temporales Modell
**Stand:** 21. Oktober 2026, 00:40 UTC (`date -u`; das Fakten-Plugin gelöscht). Davor 17. April 2026, Chat 52 (Code-Alignment)
**Pfad:** `novaberg/docs/novaberg-mem-knowledge-graph.md`
**Quellen:** nova-03-k.md, nova-03-t-a.md, nova-02-m-d.md

---

## 1. Aufgabe — Strukturiertes Wissen

Das LZG speichert Erinnerungen als Fliesstext — reich an Kontext, arm an Struktur. Der Knowledge Graph ergaenzt das LZG als strukturiertes Nachschlagewerk: Entitaeten als Nodes, Fakten als Edges (Tripel). Er beantwortet Fragen, die Fliesstext nicht kann:

- "Wo wohnt Anna?" — Vorwaertssuche ueber Subjekt + Attribut
- "Wer wohnt in Nuernberg?" — Rueckwaertssuche ueber Objekt (nur Typ 1)
- "Hat sich Annas Wohnort geaendert?" — Historienvergleich ueber bi-temporale Versionierung

Das LZG ist ein Tagebuch. Der Knowledge Graph ist ein Nachschlagewerk.

---

## 2. DB-Schema

### Entitaeten (Nodes)

```sql
CREATE TABLE entitaeten (
    id              SERIAL PRIMARY KEY,
    user_id         VARCHAR(50) NOT NULL,        -- Gedaechtnis-Partition
    name            VARCHAR(255) NOT NULL,        -- Name der Entitaet
    typ             VARCHAR(50) NOT NULL,         -- person, ort, organisation, tier, objekt, user
    zusammenfassung TEXT,                          -- Generierter Cache aus Fakten
    embedding       VECTOR(768),                  -- nomic-embed-text auf Name + Zusammenfassung
    aktiv           BOOLEAN DEFAULT TRUE,         -- Soft-Delete
    last_touched    TIMESTAMPTZ,                  -- Basis fuer Decay
    wiedervorlage_am TIMESTAMPTZ,                 -- Fuer Pixie-Butler-Task
    suchtext        TSVECTOR,                     -- Generiert aus Name + Zusammenfassung
    t_created       TIMESTAMPTZ DEFAULT NOW(),
    t_valid         TIMESTAMPTZ,
    t_invalid       TIMESTAMPTZ                   -- NULL = noch gueltig
);
```

### Fakten (Edges)

```sql
CREATE TABLE fakten (
    id              SERIAL PRIMARY KEY,
    user_id         VARCHAR(50) NOT NULL,
    subjekt_id      INTEGER REFERENCES entitaeten(id),
    attribut        VARCHAR(255) NOT NULL,        -- SCREAMING_SNAKE_CASE (WOHNT_IN, HAT_SCHWESTER)
    objekt_id       INTEGER REFERENCES entitaeten(id),  -- Typ 1 (Referenz)
    objekt_wert     TEXT,                                -- Typ 2 (Wert)
    fakt_text       TEXT,                          -- Natuerlichsprachliche Repraesentation
    embedding       VECTOR(768),                   -- Fuer semantische Suche
    aktiv           BOOLEAN DEFAULT TRUE,
    last_touched    TIMESTAMPTZ,
    t_created       TIMESTAMPTZ DEFAULT NOW(),
    t_valid         TIMESTAMPTZ,
    t_invalid       TIMESTAMPTZ,                   -- NULL = noch gueltig
    CHECK ((objekt_id IS NOT NULL AND objekt_wert IS NULL)
        OR (objekt_id IS NULL AND objekt_wert IS NOT NULL))
);
```

---

## 3. Entitaets-Typen

| Typ | Beschreibung | Beispiel |
|-----|-------------|---------|
| `person` | Menschen mit Eigennamen | Anna, Julia, Max |
| `ort` | Geographische Orte | Muenchen, Nuernberg |
| `organisation` | Firmen, Vereine, Institutionen | BMW, TU Muenchen |
| `tier` | Haustiere, benannte Tiere | Bello |
| `objekt` | Benannte Gegenstaende | Der Goldene Drache (Restaurant) |
| `user` | Die User-Entitaet (eine pro Partition) | Claus |

**Referenz vs. Interface:** Nur Eigennamen werden als Entitaeten angelegt (Referenz). Gattungsbegriffe wie "Gehirn", "Kaffee", "Lernalgorithmen" sind Interfaces und werden ignoriert. Faustregel: Kann man es auf ein Namensschild schreiben? Ja = Referenz, Nein = Interface.

---

## 4. Fakten-Typen

### Typ 1 — Referenz (Entitaet → Attribut → Entitaet)

Beide Seiten sind Entitaeten. Rueckwaertssuche moeglich.

```
Anna → WOHNT_IN → Muenchen
Claus → HAT_SCHWESTER → Anna
```

"Wer wohnt in Muenchen?" → Rueckwaertssuche ueber objekt_id moeglich.

### Typ 2 — Wert (Entitaet → Attribut → Skalar)

Das Objekt ist ein Freitext-Wert, keine Entitaet. Nur Vorwaertssuche.

```
Fujiyama → HOEHE → "ueber 3700m"
Anna → ALTER → "28 Jahre"
```

**DB-Constraint:** Ein Fakt ist immer entweder Typ 1 (objekt_id gesetzt, objekt_wert NULL) oder Typ 2 (objekt_id NULL, objekt_wert gesetzt). Nie beides.

### Typ 3 — Erinnerungen (kein Graph)

Nicht alles ist ein Fakt. "Meister war frustriert wegen eines Terminkonflikts" ist eine Erinnerung — emotional, kontextuell, narrativ. Erinnerungen bleiben im LZG als Fliesstext.

---

## 5. Fakten-Pipeline

~~Der FaktenManager (`plugins/fakten_manager/manager.py`) verarbeitet Fakten aus zwei Quellen:~~ → Das Fakten-Plugin ist am 21.10.2026 gelöscht (nicht committet); dieser Abschnitt beschreibt seinen Stand. Geschrieben wird seither vom Fakten-Agenten (`agents/fakten/`), der seine Aufträge vom Dispatcher bekommt (Schreibauftrag aus der Sachlage). Der FaktenManager verarbeitete Fakten aus zwei Quellen:

### Salienz-Pfad (implizit)

Die Salienz extrahiert Fakten als Rohformat `{subjekt, schluessel, wert, typ}`. ~~Der FaktenManager transformiert ueber Konstanten-Tabellen:~~ → Der FaktenManager transformierte über diese Konstanten-Tabellen; sie sind mit dem Plugin gelöscht:

| Tabelle | Zweck | Beispiel |
|---------|-------|---------|
| `_ENTITAETS_SCHLUESSEL` | Welche Schluessel auf Entitaets-Werte hindeuten | `schwester`, `wohnort`, `haustier` |
| `_ATTRIBUT_MAP` | Schluessel → Graph-Kanten-Name | `wohnort` → `WOHNT_IN` |
| `_WERT_TYP_MAP` | Schluessel → Typ der Wert-Entitaet | `wohnort` → `ort`, `schwester` → `person` |

### Planner-Pfad (explizit)

~~Bei Management-Befehlen ("Was weisst du ueber Anna?") kommt der Auftrag bereits im M2-Format — Entitaeten und Fakten sind strukturiert.~~ → Seit dem 21.10.2026 führt ein Router-Ziel `fakten` den Planner zu den Notizen; der Fakten-Agent nimmt keine Aufträge des Planners an (`zustellart = "queue"`).

### Verarbeitungsschritte

1. **Entity Resolution** — Entitaeten gegen DB aufloesen
2. **Neue Entitaeten anlegen** — mit Embedding (nomic-embed-text)
3. **Edge Invalidation Check** — Existiert bereits ein aktiver Fakt mit gleichem Subjekt + Attribut?
   - Kein bestehender Fakt → INSERT
   - Gleicher Wert → nur `last_touched` aktualisieren
   - Anderer Wert → alten Fakt invalidieren + neuen INSERT
4. **INSERT mit Embedding** — fakt_text-Embedding fuer semantische Suche

---

## 6. Entity Resolution

**Datei:** `memory/services/entity_resolution.py`

Entity Resolution beantwortet: Wer oder was ist gemeint? Der Service loest Namen gegen die Datenbank auf, legt neue Entitaeten an und signalisiert Rueckfrage-Bedarf.

### Algorithmus (drei Informationsquellen, von billig nach teuer)

**Schritt 0 — ICH-Aufloesung:**
```python
if name.upper() == "ICH":
    → find_by_type("user") → Claus (ID 2)
```

**Schritt 1 — Name-Match (DB, pg_trgm similarity):**
Exakte oder nahe Uebereinstimmung des Namens in der `entitaeten`-Tabelle. Schnell, deterministisch, kein LLM.

**Schritt 2 — Embedding-Match (DB + Ollama, cosine):**
Semantische Suche per pgvector. Mit Name-Plausibilitaets-Check:

```python
def _name_ist_plausibel(such_name, treffer_name):
    # Exakter Match (case-insensitive): "Anna" == "anna" → ja
    # Teilstring: "Anna" in "Anna-Maria" → ja
    # Gleicher Anfangsbuchstabe + aehnliche Laenge (+-3): "Anna" ~ "Anne" → ja
    # Komplett verschieden: "Anna" != "Max" → nein
```

Ohne den Check matchte "Anna" auf "Max" per Embedding — zwei kurze Namen mit hoher Cosine Similarity aber voellig verschiedener Bedeutung.

**Schritt 3 — Disambiguierung bei Mehrdeutigkeit:**
Wenn mehrere Kandidaten mit aehnlichem Namen existieren → Rueckfrage ueber den Responder: "Meinst du Julia aus Nuernberg oder Julia aus Augsburg?"

### Resolve-Modi

- **resolve_batch** — Liste von `{name, typ}` Paaren aufloesen (typisch nach Salienz-Extraktion)
- **resolve_single** — Einzelne Entitaet aufloesen (typisch bei Fakten-Abfragen)

### Ergebnis-Datenstruktur

```python
@dataclass
class ResolvedEntity:
    name: str              # Originalname aus der Extraktion
    typ: str               # person, ort, organisation, tier, ...
    bekannte_id: int | None  # DB-ID oder None
    ist_neu: bool          # True → muss angelegt werden
    ist_referenz: bool     # True → Referenz, False → Interface
    braucht_klaerung: bool # True → Rueckfrage noetig
    klaerungsfrage: str    # Rueckfrage-Text
    kandidaten: list[dict] # Bei Mehrdeutigkeit: moegliche Matches
```

---

## 7. Bi-temporales Modell

Jeder Fakt hat vier Zeitstempel:

| Feld | Beschreibung |
|------|-------------|
| `t_created` | Wann wurde der Fakt im System angelegt? (Systemzeit) |
| `t_valid` | Ab wann galt der Fakt in der Realwelt? |
| `t_invalid` | Ab wann galt der Fakt nicht mehr? (NULL = noch gueltig) |
| `aktiv` | Soft-Delete Flag (FALSE bei Invalidierung) |

**Kein Ueberschreiben — Update = Invalidieren + Neu Anlegen:**

```sql
-- Anna zieht von Nuernberg nach Muenchen
UPDATE fakten SET t_invalid = NOW(), aktiv = FALSE WHERE id = 47;   -- Nuernberg
INSERT INTO fakten (...) VALUES (..., 'WOHNT_IN', ..., 'Muenchen');  -- Muenchen
```

Beide Fakten bleiben in der DB:
- "Wo wohnt Anna?" → `WHERE aktiv = TRUE` → Muenchen
- "Wo hat Anna frueher gewohnt?" → Alle Fakten → Nuernberg + Muenchen mit Zeitstempeln

**Herkunft:** Das bi-temporale Modell stammt aus der Graphiti-Architektur (Zep Labs). Es loest das Ueberschreibungsproblem durch Struktur statt durch Konfidenzwerte. Gleiches Attribut bei gleicher Entitaet → Widerspruch erkennbar durch exakten Vergleich, kein Embedding-Match noetig.

---

## 8. Abfrage

### Aktive Fakten

Embedding-basierte Suche mit aktiv-Filter:

```sql
SELECT * FROM fakten
WHERE user_id = $1
  AND aktiv = TRUE
  AND t_invalid IS NULL
ORDER BY embedding <=> $query_embedding
LIMIT 10;
```

### Historie

Alle Fakten zu einer Entitaet + Attribut — aktive UND inaktive. Beantwortet "Wo hat Anna frueher gewohnt?" (zeigt den invalidierten Nuernberg-Fakt und den aktuellen Muenchen-Fakt mit Zeitstempeln).

### Enricher-Hook

~~Der FaktenManager liefert Kontext ueber `enrich()`:~~ → Das Fakten-Plugin ist am 21.10.2026 gelöscht (nicht committet); sein Hook war im Enricher abgeschaltet. Gelesen wird über die Mappe (`memory/fakten_mappe.py::load_mappe`, `FaktenRepository.find_umgebung`, Endpunkt `GET /fakten/{user_id}/{entitaet_id}`). Der Hook lieferte:

```
[Fakten/Anna (person)]
  HAT_SCHWESTER = Anna (seit 2026-03-24)
  WOHNT_IN = Muenchen (seit 2026-03-24)
```

~~Laedt~~ → Lud alle Entitaeten des Users mit ihren aktiven Fakten, formatiert als hierarchische Liste mit Gueltigkeitsdatum.

---

## Zusammenspiel (Beispiel)

```
User: "Meine Schwester Anna ist nach Muenchen gezogen"
    |
    v
Salienz: facts = [{subjekt: "ICH", schluessel: "schwester", wert: "Anna", ...}]
    |
    v
~~FaktenManager.execute()~~ → Plugin am 21.10.2026 gelöscht (nicht committet); der Ablauf zeigt seinen Stand
    |
    +-- _salienz_facts_transformieren()
    |     → Entitaeten: [ICH(user), Anna(person), Muenchen(ort)]
    |     → Fakten: [ICH HAT_SCHWESTER Anna, Anna WOHNT_IN Muenchen]
    |
    +-- Entity Resolution
    |     → ICH → Claus (ID 2)
    |     → Anna → bestehend (ID 21) oder neu
    |     → Muenchen → bestehend oder neu
    |
    +-- Edge Invalidation
    |     → Anna WOHNT_IN Nuernberg (ID 47) → invalidiert
    |
    +-- INSERT
          → Anna WOHNT_IN Muenchen (neu, mit Embedding)
```