RAG v2: Klassifizierung (6 Typen, 3 Confidence), source_type, Auto-Supersede, /memory erweitert

This commit is contained in:
root 2026-03-15 17:34:15 +07:00
parent 91a6180b5a
commit 7ae6ac0e4c
4 changed files with 108 additions and 94 deletions

View file

@ -297,20 +297,34 @@ def _tool_memory_read(scope=""):
import logging as _logging import logging as _logging
_log = _logging.getLogger("context") _log = _logging.getLogger("context")
last_suggest_result = {"type": None, "candidate_id": None} last_suggest_result = {"type": None}
VALID_MEMORY_TYPES = {"fact", "preference", "relationship", "plan", "temporary", "uncertain"}
VALID_CONFIDENCE = {"high", "medium", "low"}
NEEDS_EXPIRY = {"plan", "temporary"}
_current_source_type = "telegram_text"
def _tool_memory_suggest(scope, kind, content, memory_type="temporary", expires_at=None): def set_source_type(st: str):
global _current_source_type
_current_source_type = st
def _tool_memory_suggest(scope, kind, content, memory_type="fact", confidence="high", expires_at=None):
import memory_client import memory_client
from datetime import datetime from datetime import datetime
global last_suggest_result global last_suggest_result
_log.info("memory_suggest aufgerufen: scope=%s kind=%s type=%s content=%s", scope, kind, memory_type, content[:80])
if memory_type not in ("temporary", "permanent"): if memory_type not in VALID_MEMORY_TYPES:
memory_type = "temporary" 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 exp_epoch = None
if memory_type == "temporary": if memory_type in NEEDS_EXPIRY:
if expires_at: if expires_at:
exp_epoch = memory_client.parse_expires_from_text(expires_at) exp_epoch = memory_client.parse_expires_from_text(expires_at)
if not exp_epoch: if not exp_epoch:
@ -323,53 +337,34 @@ def _tool_memory_suggest(scope, kind, content, memory_type="temporary", expires_
"kind": kind, "kind": kind,
"content": content, "content": content,
"source": "bot-suggest", "source": "bot-suggest",
"status": "candidate", "status": "active",
"confidence": confidence,
"memory_type": memory_type, "memory_type": memory_type,
"source_type": _current_source_type,
} }
if exp_epoch: if exp_epoch:
data["expires_at"] = exp_epoch data["expires_at"] = exp_epoch
result = memory_client._post("/memory", data) result = memory_client._post("/memory", data)
prev_type = last_suggest_result.get("type")
if result and result.get("duplicate"): if result and result.get("duplicate"):
ex_status = result.get("existing_status", "?")
ex_type = result.get("existing_memory_type", "") ex_type = result.get("existing_memory_type", "")
ex_exp = result.get("existing_expires_at") ex_exp = result.get("existing_expires_at")
ex_id = result.get("existing_id") last_suggest_result = {"type": "duplicate"}
if ex_type in NEEDS_EXPIRY and ex_exp:
if ex_status == "candidate": return f"Weiss ich schon (bis {datetime.fromtimestamp(ex_exp).strftime('%d.%m.%Y')})."
if prev_type != "new_candidate": return "Weiss ich schon."
last_suggest_result = {"type": "existing_candidate", "candidate_id": ex_id}
_log.info("Duplikat: bestehender Kandidat ID=%s (prev=%s)", ex_id, prev_type)
return "Noch nicht bestaetigt — zeige Auswahl erneut."
elif ex_status == "active":
if ex_type == "temporary" and ex_exp:
exp_str = datetime.fromtimestamp(ex_exp).strftime("%d.%m.%Y")
if prev_type != "new_candidate":
last_suggest_result = {"type": "active_temporary", "candidate_id": None}
return f"Schon temporaer gespeichert bis {exp_str}."
elif ex_type == "permanent":
if prev_type != "new_candidate":
last_suggest_result = {"type": "active_permanent", "candidate_id": None}
return "Schon dauerhaft gespeichert."
else:
if prev_type != "new_candidate":
last_suggest_result = {"type": "active_other", "candidate_id": None}
return "Bereits aktiv gespeichert."
elif ex_status == "archived":
if prev_type != "new_candidate":
last_suggest_result = {"type": "existing_candidate", "candidate_id": ex_id}
memory_client._patch(f"/memory/{ex_id}", {"status": "candidate"})
return "War archiviert — erneut als Kandidat vorgeschlagen."
else:
return "Bereits vorhanden."
if result and result.get("ok"): if result and result.get("ok"):
last_suggest_result = {"type": "new_candidate", "candidate_id": None} sup = result.get("superseded_id")
type_label = "temporaer" if memory_type == "temporary" else "dauerhaft" last_suggest_result = {"type": "saved", "item_id": result.get("id"), "superseded": sup}
return f"Vorschlag gespeichert als Kandidat ({type_label})." msg = f"Gemerkt ({memory_type}, {confidence})."
return "Konnte Vorschlag nicht speichern." 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 _tool_session_search(query): def _tool_session_search(query):
@ -414,7 +409,7 @@ def get_tool_handlers(session_id: str = None) -> dict:
"get_todays_mails": lambda: _tool_get_todays_mails(), "get_todays_mails": lambda: _tool_get_todays_mails(),
"get_smart_mail_digest": lambda hours=24: _tool_get_smart_mail_digest(hours=hours), "get_smart_mail_digest": lambda hours=24: _tool_get_smart_mail_digest(hours=hours),
"memory_read": lambda scope="": _tool_memory_read(scope), "memory_read": lambda scope="": _tool_memory_read(scope),
"memory_suggest": lambda scope, kind, content, memory_type="temporary", expires_at=None: _tool_memory_suggest(scope, kind, content, memory_type=memory_type, expires_at=expires_at), "memory_suggest": lambda scope, kind, content, memory_type="fact", confidence="high", expires_at=None: _tool_memory_suggest(scope, kind, content, memory_type=memory_type, confidence=confidence, expires_at=expires_at),
"session_search": lambda query: _tool_session_search(query), "session_search": lambda query: _tool_session_search(query),
"session_summary": lambda topic="": _tool_session_summary(session_id, topic=topic) if session_id else "Keine Session aktiv.", "session_summary": lambda topic="": _tool_session_summary(session_id, topic=topic) if session_id else "Keine Session aktiv.",
} }

View file

@ -30,26 +30,28 @@ WICHTIG: Beantworte Fragen IMMER anhand dieser Fakten! Wenn der User z.B. fragt
Das Gedaechtnis ist deine primaere Wissensquelle ueber den User. Das Gedaechtnis ist deine primaere Wissensquelle ueber den User.
GEDAECHTNIS memory_suggest: GEDAECHTNIS memory_suggest:
Du MUSST memory_suggest aufrufen wenn der User etwas sagt das spaeter nuetzlich ist. Du MUSST memory_suggest aufrufen wenn der User etwas Nuetzliches sagt.
Dabei IMMER memory_type angeben: memory_suggest speichert DIREKT. Duplikate werden automatisch erkannt. Widersprueche werden automatisch aufgeloest (altes Item wird ersetzt).
TEMPORARY (memory_type="temporary") hat ein Ablaufdatum: MEMORY_TYPE immer angeben:
- Reiseplaene ("fliege nach...", "bin naechste Woche in...") - "preference" Vorlieben ("Lieblingskaffee ist Flat White", "bevorzuge dunkles Theme")
- Termine ("morgen 14 Uhr Zahnarzt", "am Freitag Meeting") - "relationship" Beziehungen/Rollen ("Ali ist Ansprechpartner fuer Wohnung")
- Kurzfristige Aufenthalte ("bin bis Mittwoch in Berlin") - "fact" Stabile Fakten ("Server IP geaendert", "Jarvis aktiv seit...")
- Einmalige Vorhaben ("will diese Woche den Server migrieren") - "plan" Konkrete Vorhaben mit Zeitbezug ("Reise nach Frankfurt am 18.03.")
Bei temporary: expires_at mit dem relevanten Zeitausdruck angeben (z.B. "naechste Woche", "morgen", "am Freitag"). - "temporary" Kurzfristige Zustaende ("bin bis Mittwoch in Berlin", "morgen Zahnarzt")
- "uncertain" Vage Aussagen ("vielleicht fliege ich frueher", "glaube Ali kuemmert sich")
PERMANENT (memory_type="permanent") bleibt dauerhaft: CONFIDENCE immer angeben:
- Persoenliche Praeferenzen ("ich bevorzuge...", "nenn mich...") - "high" Klare Aussage ("Mein Lieblingskaffee ist Flat White")
- Rollen/Beziehungen ("Ali ist mein Ansprechpartner fuer...") - "medium" Wahrscheinlich aber nicht 100% ("Ali kuemmert sich wohl darum")
- Stabile Infrastruktur-Fakten ("neuer Server heisst...", "IP geaendert auf...") - "low" Sehr vage ("vielleicht aendere ich das noch")
- Projektstatus ("Jarvis ist jetzt aktiv")
- Wiederkehrende Regeln ("API-Kosten monatlich beobachten")
Im Zweifel: lieber temporary als permanent. REGELN:
memory_suggest speichert DIREKT kein Bestaetigen noetig. Das System erkennt Duplikate automatisch. Bei Duplikaten sag kurz "Weiss ich schon." und beantworte die Frage. - Bei plan/temporary: IMMER expires_at mit Zeitausdruck angeben ("naechste Woche", "morgen")
NICHT speichern: Passwoerter, Tokens, Smalltalk, Hoeflichkeiten, reine Fragen. - Bei uncertain mit low confidence: NUR speichern wenn es trotzdem nuetzlich sein koennte
- Vages Gerede, Smalltalk, emotionale Kurzaeusserungen: NICHT speichern
- Passwoerter, Tokens: NIE speichern
- Bei Widerspruch zu bestehendem Wissen: trotzdem speichern, System erkennt und ersetzt automatisch
SESSION-RUECKBLICK: SESSION-RUECKBLICK:
- "Was haben wir besprochen?" session_summary OHNE topic - "Was haben wir besprochen?" session_summary OHNE topic
@ -303,17 +305,18 @@ TOOLS = [
"type": "function", "type": "function",
"function": { "function": {
"name": "memory_suggest", "name": "memory_suggest",
"description": "Speichert einen neuen Fakt als Kandidat. IMMER aufrufen wenn der User Reiseplaene, zeitliche Vorhaben, Projektstatus, Vorlieben oder stabile Fakten mitteilt. IMMER memory_type angeben: 'temporary' fuer zeitlich begrenzte Dinge (Reisen, Termine, einmalige Vorhaben), 'permanent' fuer stabile Fakten (Praeferenzen, Rollen, Regeln, Infrastruktur). Im Zweifel temporary.", "description": "Speichert einen Fakt direkt im Langzeitgedaechtnis. IMMER aufrufen wenn der User Reiseplaene, Termine, Vorlieben, Beziehungen, Projektstatus oder stabile Fakten mitteilt. System erkennt Duplikate und ersetzt Widersprueche automatisch.",
"parameters": { "parameters": {
"type": "object", "type": "object",
"properties": { "properties": {
"scope": {"type": "string", "enum": ["user", "environment", "project"], "description": "user=persoenlich, environment=Infrastruktur, project=Projekt"}, "scope": {"type": "string", "enum": ["user", "environment", "project"], "description": "user=persoenlich, environment=Infrastruktur, project=Projekt"},
"kind": {"type": "string", "enum": ["fact", "preference", "rule", "note"], "description": "fact=Tatsache, preference=Vorliebe, note=Notiz"}, "kind": {"type": "string", "enum": ["fact", "preference", "rule", "note"], "description": "Grobe Kategorie"},
"content": {"type": "string", "description": "Der Fakt (kurz, 3. Person, z.B. 'Fliegt naechste Woche nach Frankfurt')"}, "content": {"type": "string", "description": "Der Fakt (kurz, 3. Person, z.B. 'Lieblingskaffee ist Flat White')"},
"memory_type": {"type": "string", "enum": ["temporary", "permanent"], "description": "temporary=zeitlich begrenzt (Reisen, Termine), permanent=dauerhaft (Praeferenzen, Rollen, Regeln)"}, "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"},
"expires_at": {"type": "string", "description": "Nur bei temporary: Zeitangabe wann es ablaeuft (z.B. 'naechste Woche', 'morgen', 'am Freitag', '22.03.2026'). Leer lassen wenn unklar."}, "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"], "required": ["scope", "kind", "content", "memory_type", "confidence"],
}, },
}, },
}, },
@ -402,7 +405,7 @@ def ask_with_tools(question: str, tool_handlers: dict, session_id: str = None) -
try: try:
import memory_client import memory_client
memory_items = memory_client.get_active_memory() memory_items = memory_client.get_relevant_memory(question, top_k=10)
memory_block = memory_client.format_memory_for_prompt(memory_items) memory_block = memory_client.format_memory_for_prompt(memory_items)
except Exception: except Exception:
memory_block = "" memory_block = ""

View file

@ -248,7 +248,7 @@ def delete_candidate(item_id: int) -> bool:
def get_active_memory() -> list[dict]: def get_active_memory() -> list[dict]:
"""Holt alle aktiven Memory-Items fuer den System-Prompt (ohne abgelaufene).""" """Holt alle aktiven Memory-Items (ohne abgelaufene). Fuer /memory-Befehl."""
result = _get("/memory", {"status": "active", "limit": 100}) result = _get("/memory", {"status": "active", "limit": 100})
if not result or "items" not in result: if not result or "items" not in result:
return [] return []
@ -263,26 +263,32 @@ def get_active_memory() -> list[dict]:
return items return items
def get_relevant_memory(query: str, top_k: int = 10) -> list[dict]:
"""RAG-Suche: Holt die relevantesten Memory-Items per Vektor-Aehnlichkeit."""
result = _get("/memory/search", {"q": query, "top_k": top_k, "status": "active"})
if not result or "items" not in result:
return get_active_memory()[:top_k]
return result["items"]
def format_memory_for_prompt(items: list[dict]) -> str: def format_memory_for_prompt(items: list[dict]) -> str:
"""Formatiert Memory-Items als Text-Block fuer den System-Prompt.""" """Formatiert Memory-Items als Text-Block fuer den System-Prompt."""
if not items: if not items:
return "" return ""
permanent = [i for i in items if i.get("memory_type") != "temporary"] TYPE_ICON = {"fact": "📌", "preference": "", "relationship": "👤", "plan": "📅", "temporary": "🕒", "uncertain": ""}
temporary = [i for i in items if i.get("memory_type") == "temporary"] lines = ["", "=== GEDAECHTNIS (relevante Fakten) ==="]
for item in items:
lines = ["", "=== GEDAECHTNIS ==="] mtype = item.get("memory_type", "fact")
if permanent: icon = TYPE_ICON.get(mtype, "")
lines.append("-- Dauerhaft --") conf = item.get("confidence", "high")
for item in permanent: exp = item.get("expires_at")
lines.append(f"[{item['scope']}/{item['kind']}] {item['content']}") parts = [f"{icon} [{mtype}]"]
if temporary: if conf != "high":
lines.append("-- Temporaer --") parts.append(f"({conf})")
for item in temporary: parts.append(item["content"])
exp = item.get("expires_at") if exp:
exp_str = "" parts.append(f"(bis {datetime.fromtimestamp(exp).strftime('%d.%m.%Y')})")
if exp: lines.append(" ".join(parts))
exp_str = " (bis " + datetime.fromtimestamp(exp).strftime("%d.%m.%Y") + ")"
lines.append(f"[{item['scope']}/{item['kind']}] {item['content']}{exp_str}")
lines.append("=== ENDE GEDAECHTNIS ===") lines.append("=== ENDE GEDAECHTNIS ===")
return "\n".join(lines) return "\n".join(lines)

View file

@ -322,22 +322,30 @@ async def cmd_memory(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if not items: if not items:
await update.message.reply_text("Kein Gedaechtnis vorhanden.") await update.message.reply_text("Kein Gedaechtnis vorhanden.")
return return
permanent = [i for i in items if i.get("memory_type") != "temporary"] TYPE_ICON = {"fact": "📌", "preference": "", "relationship": "👤", "plan": "📅", "temporary": "🕒", "uncertain": ""}
temporary = [i for i in items if i.get("memory_type") == "temporary"] CONF_ICON = {"high": "", "medium": "", "low": ""}
groups = {}
for i in items:
mt = i.get("memory_type", "fact")
groups.setdefault(mt, []).append(i)
lines = [f"🧠 Gedaechtnis: {len(items)} Eintraege\n"] lines = [f"🧠 Gedaechtnis: {len(items)} Eintraege\n"]
if permanent: for mtype in ("fact", "preference", "relationship", "plan", "temporary", "uncertain"):
lines.append(f"📌 Dauerhaft ({len(permanent)}):") group = groups.get(mtype, [])
for i in permanent: if not group:
lines.append(f"{i['content'][:100]}") continue
if temporary: icon = TYPE_ICON.get(mtype, "")
lines.append(f"\n🕒 Temporaer ({len(temporary)}):") lines.append(f"{icon} {mtype.upper()} ({len(group)}):")
for i in temporary: for i in group:
conf = CONF_ICON.get(i.get("confidence", "high"), "")
src = i.get("source_type", "")
src_tag = f" [{src}]" if src and src != "manual" else ""
exp = i.get("expires_at") exp = i.get("expires_at")
exp_str = "" exp_str = ""
if exp: if exp:
from datetime import datetime from datetime import datetime
exp_str = f" (bis {datetime.fromtimestamp(exp).strftime('%d.%m.%Y')})" exp_str = f" (bis {datetime.fromtimestamp(exp).strftime('%d.%m.%Y')})"
lines.append(f"{i['content'][:100]}{exp_str}") lines.append(f"{i['content'][:90]}{conf}{exp_str}{src_tag}")
lines.append("")
text = "\n".join(lines) text = "\n".join(lines)
await update.message.reply_text(text[:4000], reply_markup=KEYBOARD) await update.message.reply_text(text[:4000], reply_markup=KEYBOARD)
@ -368,7 +376,8 @@ async def handle_voice(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
channel_key = str(update.effective_chat.id) channel_key = str(update.effective_chat.id)
session_id = memory_client.get_or_create_session(channel_key, source="telegram") session_id = memory_client.get_or_create_session(channel_key, source="telegram")
context.last_suggest_result = {"type": None, "candidate_id": None} context.last_suggest_result = {"type": None}
context.set_source_type("telegram_voice")
handlers = context.get_tool_handlers(session_id=session_id) handlers = context.get_tool_handlers(session_id=session_id)
answer = llm.ask_with_tools(text, handlers, session_id=session_id) answer = llm.ask_with_tools(text, handlers, session_id=session_id)
if session_id: if session_id:
@ -416,6 +425,7 @@ async def handle_message(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text("🤔 Denke nach...") await update.message.reply_text("🤔 Denke nach...")
try: try:
context.last_suggest_result = {"type": None} context.last_suggest_result = {"type": None}
context.set_source_type("telegram_text")
handlers = context.get_tool_handlers(session_id=session_id) handlers = context.get_tool_handlers(session_id=session_id)
answer = llm.ask_with_tools(text, handlers, session_id=session_id) answer = llm.ask_with_tools(text, handlers, session_id=session_id)
if session_id: if session_id: