Hausmeister Kumpel-Modus: OpenMemory (CT 122) angebunden

- openmemory_client.py: REST-API Client für search/add/list
- tools/openmemory.py: openmemory_add, openmemory_search, Kumpel SYSTEM_PROMPT_EXTRA
- llm.py: OpenMemory-Block in Prompt (Text + Bild)
- homelab.conf: OPENMEMORY Optionen dokumentiert
This commit is contained in:
Homelab Cursor 2026-03-24 14:03:19 +01:00
parent fe7e197c35
commit dbf2497cd6
4 changed files with 207 additions and 2 deletions

View file

@ -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},

View file

@ -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)

View file

@ -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.
"""

View file

@ -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"