feat: Konversationsgedaechtnis - Kurzzeit-Gespraechsverlauf pro Session

- Store: messages-Tabelle + append_message/get_recent_messages (mit Mandanten-Schutz)
- LLMProvider.complete um history erweitert (openrouter + local bauen system+history+user)
- Orchestrator.chat_text reicht den Verlauf ans LLM weiter
- chat.py: bei session_id letzte HISTORY_MAX_MESSAGES laden, danach User-/Assistant-Turn speichern
- ohne session_id weiterhin zustandslos; ?debug zeigt history_len
- Config HISTORY_MAX_MESSAGES (Default 10)
- Tests: 32 gruen (3 neue Gedaechtnis-Tests)
- 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:16:35 +02:00
commit 16a964032e
12 changed files with 225 additions and 40 deletions

View file

@ -2,4 +2,9 @@ from abc import ABC, abstractmethod
class LLMProvider(ABC):
@abstractmethod
async def complete(self, text: str, session_id: str | None = None) -> str: ...
async def complete(
self,
text: str,
history: list[dict] | None = None,
session_id: str | None = None,
) -> str: ...

View file

@ -7,10 +7,17 @@ class LocalOpenAICompatibleLLM(LLMProvider):
self.api_key = api_key
self.model = model
async def complete(self, text: str, session_id: str | None = None) -> str:
async def complete(
self,
text: str,
history: list[dict] | None = None,
session_id: str | None = None,
) -> str:
messages = list(history) if history else []
messages.append({"role": "user", "content": text})
payload = {
"model": self.model,
"messages": [{"role": "user", "content": text}],
"messages": messages,
"temperature": 0.3,
}
async with httpx.AsyncClient(timeout=120) as client:

View file

@ -49,7 +49,12 @@ class OpenRouterLLMProvider(LLMProvider):
self.api_key = (api_key or "").strip()
self.model = (model or "").strip()
async def complete(self, text: str, session_id: str | None = None) -> str:
async def complete(
self,
text: str,
history: list[dict] | None = None,
session_id: str | None = None,
) -> str:
if not self.api_key:
raise ValueError("OPENROUTER_API_KEY is empty")
if not self.model:
@ -57,12 +62,14 @@ class OpenRouterLLMProvider(LLMProvider):
if not text or not text.strip():
raise ValueError("LLM input text is empty")
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
if history:
messages.extend(history)
messages.append({"role": "user", "content": text.strip()})
payload = {
"model": self.model,
"messages": [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": text.strip()},
],
"messages": messages,
}
timeout = httpx.Timeout(connect=10.0, read=120.0, write=30.0, pool=10.0)