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:
parent
16a964032e
commit
531b57e08d
12 changed files with 389 additions and 9 deletions
57
app/store.py
57
app/store.py
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue