feat(memory): automatische Erinnerungs-Extraktion aus Gespraechen

- app/core/memory_extractor.py: LLM destilliert nach je N Turns dauerhafte
  Fakten/Vorlieben aus dem Verlauf, dedupliziert gegen vorhandene Erinnerungen
  und legt sie ab - best-effort, nicht-blockierend (Hintergrund-Task), eigener
  Extraktions-Prompt (JSON, Reasoning aus), Cap-Begrenzung
- Trigger in /api/chat und /ws/voice nach dem Persistieren des Turns
- Konfig: MEMORY_EXTRACTION_ENABLED/_EVERY_N_TURNS/_MAX/_PROVIDER
- Tests: Extraktion, Dedup, kaputtes JSON, Cap, leeres Gespraech, Scheduling
- Doku: README + Architektur-Roadmap (Punkt 3 erledigt)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-18 03:15:08 +02:00
commit aa64ccf585
8 changed files with 326 additions and 1 deletions

View file

@ -13,6 +13,7 @@ from app.dependencies import (
resolve_output_endpoint,
get_store,
)
from app.core.memory_extractor import maybe_schedule_extraction
from app.quota import enforce_quota, record_usage, QuotaExceededError
from app.safety.emergency import handle_emergency
from app.schemas import ChatRequest
@ -106,6 +107,7 @@ async def chat(
if session_id:
store.append_message(session_id, user.id, "user", payload.text)
store.append_message(session_id, user.id, "assistant", trace.semantic_response)
maybe_schedule_extraction(store, user.id, session_id)
if debug:
return JSONResponse(

View file

@ -29,6 +29,7 @@ from app.dependencies import (
)
from app.store import SessionOwnershipError
from app.audio.vad import EnergyVAD
from app.core.memory_extractor import maybe_schedule_extraction
from app.quota import enforce_quota, record_usage, QuotaExceededError
from app.safety.emergency import handle_emergency
@ -141,6 +142,7 @@ async def _run_turn(websocket, store, user, session_id, route, orchestrator, out
if session_id:
store.append_message(session_id, user.id, "user", text)
store.append_message(session_id, user.id, "assistant", trace.semantic_response)
maybe_schedule_extraction(store, user.id, session_id)
await websocket.send_json(
{"type": "semantic", "text": trace.semantic_response, "spoken": trace.spoken_response}