feat: Langzeit-Erinnerungen (#3b) und WebSocket-Streaming-Chat (#4, erster Increment)

#3b Langzeit-Erinnerungen:
- Store: memories-Tabelle + add/get/delete_memory (pro Nutzer)
- API: GET/POST/DELETE /api/me/memories
- chat.py injiziert Nutzer-Erinnerungen als System-Kontext ins LLM (sessionunabhaengig)
- ?debug zeigt memories_len

#4 Echtzeit (erster Increment):
- WS /ws/chat: dauerhafter Kanal, Event-Folge ack -> semantic -> audio (binaer) -> done
- Auth (Token-Query), Session-Gedaechtnis und Erinnerungen wie bei POST /api/chat
- Fehler als error-Event (422/403/502)

- Tests: 38 gruen (Erinnerungs-CRUD/Injektion, WebSocket-Streaming/Auth)
- Doku aktualisiert (README, BEDIENUNGSANLEITUNG, Architektur)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-17 04:26:55 +02:00
commit 531b57e08d
12 changed files with 389 additions and 9 deletions

View file

@ -42,6 +42,13 @@ class Session:
data: dict = field(default_factory=dict)
@dataclass
class Memory:
id: int
content: str
created_at: str = ""
class SessionOwnershipError(Exception):
"""Eine Session gehoert einem anderen Nutzer (-> HTTP 403)."""
@ -78,6 +85,18 @@ class Store(ABC):
def get_recent_messages(self, session_id: str, limit: int) -> list[dict]:
"""Liefert die letzten `limit` Nachrichten chronologisch ([{'role','content'}, ...])."""
@abstractmethod
def add_memory(self, user_id: str, content: str) -> Memory:
"""Speichert eine dauerhafte Erinnerung (Fakt/Vorliebe) zum Nutzer."""
@abstractmethod
def get_memories(self, user_id: str) -> list[Memory]:
"""Liefert alle Erinnerungen des Nutzers (chronologisch)."""
@abstractmethod
def delete_memory(self, user_id: str, memory_id: int) -> bool:
"""Loescht eine Erinnerung des Nutzers. True, wenn etwas geloescht wurde."""
class SQLiteStore(Store):
def __init__(self, db_path: str):
@ -119,6 +138,14 @@ class SQLiteStore(Store):
);
CREATE INDEX IF NOT EXISTS idx_messages_session
ON messages(session_id, id);
CREATE TABLE IF NOT EXISTS memories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
content TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_memories_user
ON memories(user_id, id);
"""
)
@ -244,3 +271,33 @@ class SQLiteStore(Store):
(session_id, limit),
).fetchall()
return [{"role": row["role"], "content": row["content"]} for row in reversed(rows)]
# ----- Langzeit-Erinnerungen --------------------------------------------
def add_memory(self, user_id: str, content: str) -> Memory:
now = _now()
with self._connect() as conn:
cur = conn.execute(
"INSERT INTO memories (user_id, content, created_at) VALUES (?, ?, ?)",
(user_id, content, now),
)
memory_id = cur.lastrowid
return Memory(id=memory_id, content=content, created_at=now)
def get_memories(self, user_id: str) -> list[Memory]:
with self._connect() as conn:
rows = conn.execute(
"SELECT id, content, created_at FROM memories WHERE user_id = ? ORDER BY id",
(user_id,),
).fetchall()
return [
Memory(id=row["id"], content=row["content"], created_at=row["created_at"])
for row in rows
]
def delete_memory(self, user_id: str, memory_id: int) -> bool:
with self._connect() as conn:
cur = conn.execute(
"DELETE FROM memories WHERE id = ? AND user_id = ?",
(memory_id, user_id),
)
return cur.rowcount > 0