diff --git a/homelab-ai-bot/llm.py b/homelab-ai-bot/llm.py index 48bc3845..113f6825 100644 --- a/homelab-ai-bot/llm.py +++ b/homelab-ai-bot/llm.py @@ -349,8 +349,14 @@ def ask_with_tools(question: str, tool_handlers: dict, session_id: str = None) - except Exception: memory_block = "" + try: + import openmemory_client + openmemory_block = openmemory_client.get_openmemory_for_prompt(question, top_k=8) + except Exception: + openmemory_block = "" + _extra = tool_loader.get_extra_prompt() - _full_prompt = SYSTEM_PROMPT + ("\n\n" + _extra if _extra else "") + memory_block + _full_prompt = SYSTEM_PROMPT + ("\n\n" + _extra if _extra else "") + memory_block + (("\n" + openmemory_block) if openmemory_block else "") messages = [ {"role": "system", "content": _full_prompt}, @@ -474,6 +480,13 @@ def ask_with_image(image_base64: str, caption: str, tool_handlers: dict, session except Exception: memory_block = "" + try: + import openmemory_client + q = caption if caption else "Bild-Analyse" + openmemory_block = openmemory_client.get_openmemory_for_prompt(q, top_k=8) + except Exception: + openmemory_block = "" + default_prompt = ( "Analysiere dieses Bild. " "Wenn es ein Dokument ist (Ticket, Rechnung, Beleg, Buchung): " @@ -489,7 +502,7 @@ def ask_with_image(image_base64: str, caption: str, tool_handlers: dict, session ] _extra = tool_loader.get_extra_prompt() - _full_prompt = SYSTEM_PROMPT + ("\n\n" + _extra if _extra else "") + memory_block + _full_prompt = SYSTEM_PROMPT + ("\n\n" + _extra if _extra else "") + memory_block + (("\n" + openmemory_block) if openmemory_block else "") messages = [ {"role": "system", "content": _full_prompt}, diff --git a/homelab-ai-bot/openmemory_client.py b/homelab-ai-bot/openmemory_client.py new file mode 100644 index 00000000..00a98e07 --- /dev/null +++ b/homelab-ai-bot/openmemory_client.py @@ -0,0 +1,105 @@ +"""OpenMemory REST-Client (CT 122). + +Nutzt die REST-API von OpenMemory (mem0/Qdrant) für langfristige User-Memories. +Ergänzt den Memory-Service (CT 117) um semantische/langfristige Erinnerungen. +""" + +import logging +from typing import Optional + +import requests + +from core import config + +log = logging.getLogger("openmemory_client") + +_cfg = None +_base_url = None +_user_id = None + + +def _ensure_config(): + global _cfg, _base_url, _user_id + if _base_url is not None: + return + _cfg = config.parse_config() + _base_url = (_cfg.raw.get("OPENMEMORY_API_URL") or "http://10.10.10.122:8765").rstrip("/") + _user_id = _cfg.raw.get("OPENMEMORY_USER_ID") or "orbitalo" + if not _base_url: + log.warning("OPENMEMORY_API_URL nicht in homelab.conf") + + +def search(query: str, limit: int = 10) -> list[dict]: + """Sucht Memories per Text (GET mit search_query). Liefert Liste von {content, id, ...}.""" + _ensure_config() + if not _base_url: + return [] + try: + r = requests.get( + f"{_base_url}/api/v1/memories/", + params={"user_id": _user_id, "search_query": query, "size": limit}, + timeout=5, + ) + if r.ok: + data = r.json() + items = data.get("items", []) + return [{"content": i.get("content", ""), "id": str(i.get("id", ""))} for i in items] + log.debug("OpenMemory search %s: %s", r.status_code, r.text[:150]) + except Exception as e: + log.debug("OpenMemory search: %s", e) + return [] + + +def add(text: str, app: str = "hausmeister") -> Optional[dict]: + """Fügt eine neue Memory hinzu. Gibt {id, ...} oder None zurück.""" + _ensure_config() + if not _base_url or not text.strip(): + return None + try: + r = requests.post( + f"{_base_url}/api/v1/memories/", + json={"user_id": _user_id, "text": text.strip(), "metadata": {}, "infer": True, "app": app}, + timeout=10, + ) + if r.ok: + return r.json() + log.warning("OpenMemory add %s: %s", r.status_code, r.text[:200]) + except Exception as e: + log.warning("OpenMemory add: %s", e) + return None + + +def list_memories(limit: int = 15) -> list[dict]: + """Listet die neuesten Memories (ohne semantische Suche).""" + _ensure_config() + if not _base_url: + return [] + try: + r = requests.get( + f"{_base_url}/api/v1/memories/", + params={"user_id": _user_id, "page": 1, "size": limit}, + timeout=5, + ) + if r.ok: + data = r.json() + items = data.get("items", []) + return [{"content": i.get("content", ""), "id": str(i.get("id", ""))} for i in items] + except Exception as e: + log.debug("OpenMemory list: %s", e) + return [] + + +def get_openmemory_for_prompt(query: str, top_k: int = 8) -> str: + """Holt OpenMemory-Ergebnisse und formatiert sie für den System-Prompt.""" + items = search(query, limit=top_k) + if not items: + items = list_memories(limit=5) + if not items: + return "" + lines = ["", "=== OPENMEMORY (langfristige Erinnerungen) ==="] + for item in items: + c = (item.get("content") or "").strip() + if c: + lines.append(f"• {c}") + lines.append("=== ENDE OPENMEMORY ===") + return "\n".join(lines) diff --git a/homelab-ai-bot/tools/openmemory.py b/homelab-ai-bot/tools/openmemory.py new file mode 100644 index 00000000..e24565a5 --- /dev/null +++ b/homelab-ai-bot/tools/openmemory.py @@ -0,0 +1,82 @@ +"""OpenMemory Tools — Langfristiges Gedächtnis (CT 122). + +Ergänzt tools/memory.py: OpenMemory speichert Erinnerungen/Vorlieben +für den Kumpel-Modus (warnen, erinnern, nachfragen). +""" + +import logging + +from openmemory_client import add as _om_add, search as _om_search + +_log = logging.getLogger("tools.openmemory") + +TOOLS = [ + { + "type": "function", + "function": { + "name": "openmemory_add", + "description": "Speichert eine langfristige Erinnerung oder Vorliebe in OpenMemory. Nutze dies für: persönliche Fakten, Vorlieben, wichtige Termine, Vereinbarungen die du im Gespräch erfährst. Nicht für technische Homelab-Fakten (dafür memory_suggest).", + "parameters": { + "type": "object", + "properties": { + "text": {"type": "string", "description": "Der zu speichernde Inhalt"}, + }, + "required": ["text"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "openmemory_search", + "description": "Sucht in OpenMemory nach Erinnerungen/Vorlieben zu einem Thema. Nutze wenn du gezielt nach etwas suchst.", + "parameters": { + "type": "object", + "properties": { + "query": {"type": "string", "description": "Suchbegriffe"}, + }, + "required": ["query"], + }, + }, + }, +] + + +def _handle_openmemory_add(text, **kw): + if not text or not str(text).strip(): + return "Kein Text zum Speichern." + result = _om_add(str(text).strip()) + if result: + return "In OpenMemory gespeichert." + return "OpenMemory nicht erreichbar oder Speichern fehlgeschlagen." + + +def _handle_openmemory_search(query, **kw): + if not query or not str(query).strip(): + return "Kein Suchbegriff." + items = _om_search(str(query).strip(), limit=8) + if not items: + return f"Keine OpenMemory-Einträge für '{query}' gefunden." + lines = [f"OpenMemory Suche '{query}':"] + for i in items: + c = (i.get("content") or "").strip() + if c: + lines.append(f" • {c}") + return "\n".join(lines) + + +def get_handlers(session_id=None): + return { + "openmemory_add": _handle_openmemory_add, + "openmemory_search": _handle_openmemory_search, + } + + +SYSTEM_PROMPT_EXTRA = """ +KUMPEL-MODUS (kritischer Freund): +Du kennst den User aus langfristigen Erinnerungen (OpenMemory) und dem strukturierten Gedächtnis. +- ERINNERN: Wenn du weisst dass etwas ansteht (Termin, Plan), erinnere kurz daran wenn es passt. +- WARNEN: Bei riskanten Aktionen (löschen, umkonfigurieren) kurz nachfragen oder warnen. +- NACHFRAGEN: Bei vagen Aussagen ("vielleicht", "irgendwann") optional nachfragen — aber nicht übertreiben. +- Tonalität: Direkt wie ein Kumpel, nicht unterwürfig. Kurz. +""" diff --git a/homelab.conf b/homelab.conf index 4e802ea7..67d7a1c4 100644 --- a/homelab.conf +++ b/homelab.conf @@ -299,3 +299,8 @@ TUNNEL_601_MU3="rss-manager|:8080|standby" # helmut-pve → pve-he (Ramsin, bei Helmut) # PBS → pbs-mu (PBS Muldenstein) # ============================================================ + +# OpenMemory (CT 122) - optional, Default: http://10.10.10.122:8765 +# OPENMEMORY_API_URL="http://10.10.10.122:8765" +# OPENMEMORY_USER_ID="orbitalo" +