docs: add design spec for cross-chat memory (facts + summaries)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019iZMEPk2Kt1UC96Lo9bC5w
This commit is contained in:
commit
32026d4264
|
|
@ -0,0 +1,144 @@
|
|||
# JARVIS Chatuebergreifendes Gedaechtnis - Design
|
||||
|
||||
**Datum:** 2026-09-13
|
||||
**Status:** Approved, bereit fuer Implementierungsplan
|
||||
|
||||
## Kontext
|
||||
|
||||
Der Chat kennt bisher nur den Verlauf der eigenen `conversation_id`
|
||||
(`main.py:754`, `get_messages(conversation_id)`). Startet man einen neuen
|
||||
Chat (z.B. ueber den neuen "Neuer Chat"-Button), gehen alle Infos aus
|
||||
frueheren Unterhaltungen verloren - JARVIS "vergisst" Namen, Vorlieben und
|
||||
Kontext aus vorherigen Gespraechen. Ziel dieser Phase: ein Gedaechtnis, das
|
||||
ueber einzelne Conversations hinweg besteht, kombiniert aus zwei Quellen:
|
||||
|
||||
- **Explizite Fakten** - Dinge, die der Nutzer ausdruecklich festhalten
|
||||
laesst ("merke dir...", "vergiss, dass...")
|
||||
- **Automatische Zusammenfassungen** - jede Conversation wird laufend
|
||||
zusammengefasst und per Aehnlichkeitssuche wiedergefunden, wenn sie zum
|
||||
aktuellen Thema passt
|
||||
|
||||
Die vorhandene Knowledge-Base-Infrastruktur (Phase 3a: `pgvector`, Ollama
|
||||
`nomic-embed-text` Embeddings) wird fuer die Zusammenfassungen wiederverwendet
|
||||
statt eine zweite Vector-Search-Loesung einzufuehren.
|
||||
|
||||
## Datenmodell (`migrations/006_memory.sql`)
|
||||
|
||||
```sql
|
||||
CREATE TABLE memory_facts (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
content TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE conversation_summaries (
|
||||
conversation_id INTEGER PRIMARY KEY REFERENCES conversations(id),
|
||||
summary TEXT NOT NULL,
|
||||
embedding vector(768) NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
Kein `ivfflat`/`hnsw`-Index vorerst - gleiche Begruendung wie bei
|
||||
`document_chunks` (Phase 3a): bei wenig Zeilen liefert der Index 0 Treffer,
|
||||
Sequential Scan ist bei der zu erwartenden Groessenordnung (Dutzende bis
|
||||
niedrige Hunderte Conversations) schnell genug. Nachtrag folgt, sobald genug
|
||||
Daten vorhanden sind (gleicher offene Punkt wie bei den Dokumenten).
|
||||
|
||||
## Fakten erfassen: zwei neue Claude-Tools
|
||||
|
||||
Analog zu Kalender/E-Mail in `run_chat_completion()`:
|
||||
|
||||
- `remember_fact(fact: str)` - legt sofort einen Eintrag in `memory_facts`
|
||||
an, **keine Rueckfrage noetig** (wie `create_calendar_event`) - ein
|
||||
gespeicherter Fakt ist folgenlos und leicht wieder zu entfernen.
|
||||
- `forget_fact(query: str)` - sucht per `ILIKE '%query%'` nach passenden
|
||||
Fakten. Bei einem Treffer: loescht ihn erst **nach Bestaetigung im Chat**
|
||||
(wie `delete_calendar_event`/`send_email`). Bei mehreren Treffern: gibt sie
|
||||
Claude als Liste zurueck, damit es im Chat nachfragt, welcher gemeint ist,
|
||||
statt blind zu loeschen. Bei keinem Treffer: klare Fehlermeldung an Claude
|
||||
("kein passender Fakt gefunden").
|
||||
|
||||
Der System-Prompt bekommt eine kurze Ergaenzung, die Claude anweist, diese
|
||||
Tools zu nutzen, wenn der Nutzer explizit um Merken/Vergessen bittet -
|
||||
analog zu den bestehenden Anweisungen fuer Kalender-Loeschen.
|
||||
|
||||
## Automatische Zusammenfassung
|
||||
|
||||
Nach dem Speichern der Assistant-Antwort in `POST /api/v1/chat`
|
||||
(nach `save_message(..., "assistant", ...)`) wird per `asyncio.create_task`
|
||||
ein Hintergrund-Job gestartet, der die Chat-Antwort **nicht** blockiert:
|
||||
|
||||
1. Vollstaendigen Verlauf der Conversation laden (`get_messages`)
|
||||
2. Kurzer zusaetzlicher Claude-Call (kleines `max_tokens`-Limit): "Fasse
|
||||
dieses Gespraech in 2-3 Saetzen zusammen"
|
||||
3. Zusammenfassung mit der vorhandenen Ollama-Embedding-Funktion (aus Phase
|
||||
3a, bereits fuer `document_chunks` genutzt) embedden
|
||||
4. `INSERT INTO conversation_summaries (...) VALUES (...) ON CONFLICT
|
||||
(conversation_id) DO UPDATE SET summary = ..., embedding = ...,
|
||||
updated_at = CURRENT_TIMESTAMP`
|
||||
|
||||
Laeuft nach **jeder** Turn-Antwort (kein Batching/Intervall-Logik noetig -
|
||||
bei diesem Nutzungsvolumen vernachlaessigbare Zusatzkosten, ein Client
|
||||
wartet nicht darauf). Ergebnis ist immer die Zusammenfassung des gesamten
|
||||
bisherigen Gespraechs, nicht nur der letzten Nachricht.
|
||||
|
||||
## Einbindung in den Chat (`run_chat_completion`)
|
||||
|
||||
Vor dem eigentlichen Claude-Call wird der System-Prompt um zwei Abschnitte
|
||||
ergaenzt:
|
||||
|
||||
1. **"Bekannte Fakten ueber den Nutzer"** - alle Zeilen aus `memory_facts`
|
||||
werden vollstaendig geladen und als Liste angehaengt (kleine Tabelle,
|
||||
kein Retrieval noetig - Vollstaendigkeit hat hier Vorrang vor
|
||||
Relevanz-Filterung, damit kein gemerkter Fakt durch eine unguenstige
|
||||
Formulierung der aktuellen Frage durchrutscht).
|
||||
2. **"Relevante fruehere Gespraeche"** - die aktuelle Nutzernachricht wird
|
||||
embedded (gleiche Ollama-Funktion), die Top-3 aehnlichsten Zeilen aus
|
||||
`conversation_summaries` per Cosinus-Distanz geholt (`<=>`-Operator wie
|
||||
bei der Dokumentensuche), **exklusive der aktuellen Conversation** (die
|
||||
steht ja bereits vollstaendig in der History).
|
||||
|
||||
Beide Abschnitte werden nur angehaengt, wenn sie nicht leer sind.
|
||||
|
||||
## Fehlerbehandlung
|
||||
|
||||
- Ollama nicht erreichbar waehrend der Retrieval-Suche (Embedding der
|
||||
aktuellen Nachricht schlaegt fehl): Abschnitt "Relevante fruehere
|
||||
Gespraeche" wird einfach weggelassen, der Chat laeuft normal weiter
|
||||
(gleiches Muster wie bestehende `check_ollama()`-Fehlerbehandlung).
|
||||
- Hintergrund-Summary-Task wirft eine Exception (Claude-Call oder Embedding
|
||||
schlaegt fehl): wird geloggt, hat **keinen** Einfluss auf die bereits an
|
||||
den Nutzer gesendete Chat-Antwort (Task laeuft komplett nach dem Response
|
||||
Return).
|
||||
- `forget_fact` ohne Treffer: kein Fehler, sondern eine normale
|
||||
Tool-Antwort an Claude, damit es im Chat mitteilt, dass nichts gefunden
|
||||
wurde.
|
||||
|
||||
## Testing
|
||||
|
||||
- Unit-Tests (`tests/`, gemockte DB wie bei bestehenden Tool-Tests) fuer:
|
||||
- `remember_fact`/`forget_fact` (Insert, ILIKE-Suche, Mehrfachtreffer,
|
||||
kein Treffer)
|
||||
- Prompt-Zusammensetzung: Facts-Abschnitt korrekt formatiert, leer wenn
|
||||
keine Fakten vorhanden; Retrieval-Abschnitt korrekt formatiert, leer
|
||||
wenn Embedding fehlschlaegt
|
||||
- Manueller End-to-End-Test nach Deployment: in Chat A "merke dir, dass
|
||||
mein Hund Bruno heisst" sagen, ueber den "Neuer Chat"-Button einen neuen
|
||||
Chat B starten, dort nach dem Hundenamen fragen (Fakt-Pfad) und ueber ein
|
||||
thematisch verwandtes, aber nicht identisches Thema aus Chat A fragen
|
||||
(Zusammenfassungs-Retrieval-Pfad); anschliessend "vergiss, dass mein Hund
|
||||
Bruno heisst" in einem dritten Chat testen.
|
||||
|
||||
## Out of Scope (bewusst nicht Teil dieser Phase)
|
||||
|
||||
- Automatisches Bereinigen alter/widerspruechlicher Fakten (z.B. wenn sich
|
||||
eine Info spaeter aendert, muss der Nutzer explizit `forget_fact` +
|
||||
`remember_fact` nutzen).
|
||||
- `ivfflat`/`hnsw`-Index fuer `conversation_summaries` (Nachtrag, sobald
|
||||
Datenmenge es rechtfertigt - siehe Phase 3a Follow-up).
|
||||
- Mehrere Nutzer/Rollen (weiterhin ein einzelner `DEFAULT_USER_ID`, siehe
|
||||
bestehende "Security"-Einschraenkung in der Handoff-Doku).
|
||||
- UI zum Anzeigen/Verwalten der gespeicherten Fakten im Dashboard (aktuell
|
||||
nur ueber Chat-Kommandos verwaltbar).
|
||||
Loading…
Reference in New Issue