homelab-brain/homelab-ai-bot/tools/memory.py

183 lines
7 KiB
Python

"""Memory/RAG Tools — Gedaechtnis, Sessions, Fakten-Speicherung."""
import logging
_log = logging.getLogger("tools.memory")
VALID_MEMORY_TYPES = {"fact", "preference", "relationship", "plan", "temporary", "uncertain"}
VALID_CONFIDENCE = {"high", "medium", "low"}
NEEDS_EXPIRY = {"plan", "temporary"}
last_suggest_result = {"type": None}
_current_source_type = "telegram_text"
def set_source_type(st: str):
global _current_source_type
_current_source_type = st
TOOLS = [
{
"type": "function",
"function": {
"name": "memory_read",
"description": "Liest gespeicherte Fakten/Erinnerungen. Optional nach Scope filtern (z.B. 'user', 'system').",
"parameters": {
"type": "object",
"properties": {
"scope": {"type": "string", "description": "Filter nach Scope (optional)", "default": ""}
},
"required": [],
},
},
},
{
"type": "function",
"function": {
"name": "memory_suggest",
"description": "Speichert einen neuen Fakt/Erinnerung. Duplikate werden automatisch erkannt, Widersprueche aufgeloest.",
"parameters": {
"type": "object",
"properties": {
"scope": {"type": "string", "description": "Bereich: 'user', 'homelab', 'project'"},
"kind": {"type": "string", "description": "Unterkategorie: 'reise', 'server', 'kontakt', 'todo', 'allgemein'"},
"content": {"type": "string", "description": "Der zu speichernde Fakt"},
"memory_type": {"type": "string", "enum": ["fact", "preference", "relationship", "plan", "temporary", "uncertain"], "description": "fact=stabiler Fakt, preference=Vorliebe, relationship=Beziehung/Rolle, plan=Vorhaben mit Zeitbezug, temporary=kurzfristiger Zustand, uncertain=vage Aussage"},
"confidence": {"type": "string", "enum": ["high", "medium", "low"], "description": "high=klare Aussage, medium=wahrscheinlich, low=vage"},
"expires_at": {"type": "string", "description": "Bei plan/temporary: Zeitangabe wann es ablaeuft (z.B. 'naechste Woche', 'morgen', '22.03.2026'). Leer bei dauerhaften Fakten."},
},
"required": ["scope", "kind", "content", "memory_type", "confidence"],
},
},
},
{
"type": "function",
"function": {
"name": "session_search",
"description": "Volltextsuche in vergangenen Sessions nach konkreten Stichworten. Fuer gezielte Suche wie 'Was habe ich ueber Backup gesagt?' oder 'Wann war das mit Seafile?'.",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Suchbegriffe"},
},
"required": ["query"],
},
},
},
{
"type": "function",
"function": {
"name": "session_summary",
"description": "Zusammenfassung der aktuellen Session. Ohne topic = alle Themen. Mit topic = nur thematisch passende Punkte.",
"parameters": {
"type": "object",
"properties": {
"topic": {"type": "string", "description": "Themenbegriff zum Filtern (optional)"},
},
"required": [],
},
},
},
]
def _handle_memory_read(scope="", **kw):
import memory_client
items = memory_client.get_active_memory()
if scope:
items = [i for i in items if i.get("scope") == scope]
if not items:
return "Keine Memory-Eintraege gefunden."
lines = []
for i in items:
lines.append(f"[{i['scope']}/{i['kind']}] {i['content']}")
return "\n".join(lines)
def _handle_memory_suggest(scope, kind, content, memory_type="fact", confidence="high", expires_at=None, **kw):
import memory_client
from datetime import datetime
global last_suggest_result
if memory_type not in VALID_MEMORY_TYPES:
memory_type = "fact"
if confidence not in VALID_CONFIDENCE:
confidence = "high"
_log.info("memory_suggest: type=%s conf=%s src=%s content=%s",
memory_type, confidence, _current_source_type, content[:80])
exp_epoch = None
if memory_type in NEEDS_EXPIRY:
if expires_at:
exp_epoch = memory_client.parse_expires_from_text(expires_at)
if not exp_epoch:
exp_epoch = memory_client.parse_expires_from_text(content)
if not exp_epoch:
exp_epoch = memory_client.default_expires()
data = {
"scope": scope,
"kind": kind,
"content": content,
"source": "bot-suggest",
"status": "active",
"confidence": confidence,
"memory_type": memory_type,
"source_type": _current_source_type,
}
if exp_epoch:
data["expires_at"] = exp_epoch
result = memory_client._post("/memory", data)
if result and result.get("duplicate"):
ex_type = result.get("existing_memory_type", "")
ex_exp = result.get("existing_expires_at")
last_suggest_result = {"type": "duplicate"}
if ex_type in NEEDS_EXPIRY and ex_exp:
return f"Weiss ich schon (bis {datetime.fromtimestamp(ex_exp).strftime('%d.%m.%Y')})."
return "Weiss ich schon."
if result and result.get("ok"):
sup = result.get("superseded_id")
last_suggest_result = {"type": "saved", "item_id": result.get("id"), "superseded": sup}
msg = f"Gemerkt ({memory_type}, {confidence})."
if sup:
msg += f" Alten Eintrag #{sup} ersetzt."
_log.info("Superseded: #%s -> #%s", sup, result.get("id"))
_log.info("Gespeichert: ID=%s type=%s conf=%s", result.get("id"), memory_type, confidence)
return msg
return "Konnte nicht speichern."
def _handle_session_search(query, **kw):
import memory_client
result = memory_client._get("/sessions/search", {"q": query, "limit": 20})
if not result or not result.get("results"):
return f"Keine Ergebnisse fuer '{query}'."
lines = [f"Suche '{query}': {result['count']} Treffer"]
for r in result["results"][:10]:
role = r.get("role", "?")
content = (r.get("content") or "")[:150]
lines.append(f" [{role}] {content}")
return "\n".join(lines)
def _handle_session_summary(session_id, topic=None, **kw):
import memory_client
if not session_id:
return "Keine Session aktiv."
return memory_client.get_session_summary(session_id, limit=20, topic=topic or None)
def get_handlers(session_id=None):
"""Factory — session_id wird fuer session_summary gebraucht."""
return {
"memory_read": _handle_memory_read,
"memory_suggest": _handle_memory_suggest,
"session_search": _handle_session_search,
"session_summary": lambda topic="", **kw: _handle_session_summary(session_id, topic=topic),
}