From f253b5d41015c7b144a53812140ea4efa508d407 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 15 Mar 2026 11:23:28 +0700 Subject: [PATCH] Memory-Service: Client + Session-Logging + Memory-Injection fuer Hausmeister-Bot --- homelab-ai-bot/llm.py | 9 ++- homelab-ai-bot/memory_client.py | 113 ++++++++++++++++++++++++++++++++ homelab-ai-bot/telegram_bot.py | 8 +++ homelab.conf | 3 + 4 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 homelab-ai-bot/memory_client.py diff --git a/homelab-ai-bot/llm.py b/homelab-ai-bot/llm.py index de2aa313..d6e389ea 100644 --- a/homelab-ai-bot/llm.py +++ b/homelab-ai-bot/llm.py @@ -296,8 +296,15 @@ def ask_with_tools(question: str, tool_handlers: dict) -> str: if not api_key: return "OpenRouter API Key fehlt in homelab.conf" + try: + import memory_client + memory_items = memory_client.get_active_memory() + memory_block = memory_client.format_memory_for_prompt(memory_items) + except Exception: + memory_block = "" + messages = [ - {"role": "system", "content": SYSTEM_PROMPT}, + {"role": "system", "content": SYSTEM_PROMPT + memory_block}, {"role": "user", "content": question}, ] diff --git a/homelab-ai-bot/memory_client.py b/homelab-ai-bot/memory_client.py new file mode 100644 index 00000000..5e558d93 --- /dev/null +++ b/homelab-ai-bot/memory_client.py @@ -0,0 +1,113 @@ +"""Client fuer den Memory-Service (CT 117). + +Stellt Session-Management und Memory-Zugriff bereit. +Kein Import von Bot- oder LLM-Logik — reiner HTTP-Client. +""" + +import logging +import time +import uuid +from typing import Optional + +import requests + +from core import config + +log = logging.getLogger("memory_client") + +_cfg = None +_base_url = None +_token = None + +SESSION_TIMEOUT = 1800 # 30 Minuten Inaktivitaet = neue Session +_active_sessions: dict[str, dict] = {} # channel_key -> {id, last_activity} + + +def _ensure_config(): + global _cfg, _base_url, _token + if _base_url: + return + _cfg = config.parse_config() + _base_url = _cfg.raw.get("MEMORY_API_URL", "").rstrip("/") + _token = _cfg.raw.get("MEMORY_API_TOKEN", "") + if not _base_url or not _token: + log.warning("MEMORY_API_URL oder MEMORY_API_TOKEN nicht in homelab.conf") + + +def _headers(): + return {"Authorization": f"Bearer {_token}", "Content-Type": "application/json"} + + +def _post(path: str, data: dict) -> Optional[dict]: + _ensure_config() + if not _base_url: + return None + try: + r = requests.post(f"{_base_url}{path}", json=data, headers=_headers(), timeout=5) + if r.ok: + return r.json() + log.warning("Memory API %s: %s %s", path, r.status_code, r.text[:200]) + except Exception as e: + log.warning("Memory API %s: %s", path, e) + return None + + +def _get(path: str, params: dict = None) -> Optional[dict]: + _ensure_config() + if not _base_url: + return None + try: + r = requests.get(f"{_base_url}{path}", params=params, headers=_headers(), timeout=5) + if r.ok: + return r.json() + log.warning("Memory API %s: %s %s", path, r.status_code, r.text[:200]) + except Exception as e: + log.warning("Memory API %s: %s", path, e) + return None + + +def get_or_create_session(channel_key: str, source: str = "telegram") -> Optional[str]: + """Gibt eine aktive Session-ID zurueck oder erstellt eine neue.""" + now = time.time() + cached = _active_sessions.get(channel_key) + if cached and (now - cached["last_activity"]) < SESSION_TIMEOUT: + cached["last_activity"] = now + return cached["id"] + + result = _post("/sessions", {"source": source, "channel_key": channel_key}) + if result and "id" in result: + _active_sessions[channel_key] = {"id": result["id"], "last_activity": now} + return result["id"] + return None + + +def log_message(session_id: str, role: str, content: str, source: str = None, meta: str = None): + """Speichert eine Nachricht in der Session.""" + if not session_id or not content: + return + data = {"role": role, "content": content} + if source: + data["source"] = source + if meta: + data["meta_json"] = meta + _post(f"/sessions/{session_id}/messages", data) + + +def get_active_memory() -> list[dict]: + """Holt alle aktiven Memory-Items fuer den System-Prompt.""" + result = _get("/memory", {"status": "active", "limit": 100}) + if result and "items" in result: + return result["items"] + return [] + + +def format_memory_for_prompt(items: list[dict]) -> str: + """Formatiert Memory-Items als Text-Block fuer den System-Prompt.""" + if not items: + return "" + lines = ["", "=== GEDAECHTNIS (persistente Fakten) ==="] + for item in items: + prefix = f"[{item['scope']}/{item['kind']}]" + lines.append(f"{prefix} {item['content']}") + lines.append("=== ENDE GEDAECHTNIS ===") + return "\n".join(lines) diff --git a/homelab-ai-bot/telegram_bot.py b/homelab-ai-bot/telegram_bot.py index a6b6e417..09078370 100644 --- a/homelab-ai-bot/telegram_bot.py +++ b/homelab-ai-bot/telegram_bot.py @@ -77,6 +77,7 @@ BUTTON_MAP = { import context import requests as _req import llm +import memory_client import monitor from core import config @@ -305,10 +306,17 @@ async def handle_message(update: Update, ctx: ContextTypes.DEFAULT_TYPE): elif cmd == "silence": return await cmd_silence(update, ctx) + channel_key = str(update.effective_chat.id) + session_id = memory_client.get_or_create_session(channel_key, source="telegram") + if session_id: + memory_client.log_message(session_id, "user", text) + await update.message.reply_text("🤔 Denke nach...") try: handlers = context.get_tool_handlers() answer = llm.ask_with_tools(text, handlers) + if session_id: + memory_client.log_message(session_id, "assistant", answer) await update.message.reply_text(answer[:4000], reply_markup=KEYBOARD) except Exception as e: log.exception("Fehler bei Freitext") diff --git a/homelab.conf b/homelab.conf index 6ac4bbb9..7e3eade5 100644 --- a/homelab.conf +++ b/homelab.conf @@ -88,6 +88,7 @@ CT_112_HZ="fuenfvoracht|100.73.171.62|FuenfVorAcht Telegram Bot" CT_113_HZ="redax-wp|100.69.243.16|Redakteur WordPress KI-Autor + DeutschlandBlog" CT_115_HZ="flugscanner-hub|100.92.161.97|Flugpreisscanner Hub + Scheduler" CT_116_HZ="homelab-ai-bot|100.123.47.7|Hausmeister Telegram Bot" +CT_117_HZ="memory-service|100.121.192.94|Memory Service API (FastAPI + SQLite)" CT_144_HZ="muldenstein-backup|—|Backup-Archiv (Read-Only)" CT_999_HZ="cluster-docu|100.79.8.49|Dokumentation" @@ -175,6 +176,8 @@ FORGEJO_TOKEN="b874766bdf357bd4c32fa4369d0c588fc6193336" FORGEJO_SYNC_TOKEN="5402da0447b0eb6aede721a8748a08974ddc5c42" GITHUB_PAT="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" OPENROUTER_KEY="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" +MEMORY_API_TOKEN="Ai8eeQibV6Z1RWc7oNPim4PXB4vILU1nRW2-XgRcX2M" +MEMORY_API_URL="http://100.121.192.94:8400" # --- HOMELAB MCP-SERVER (auf pve-hetzner Host) --- MCP_PATH="/root/homelab-mcp"