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
_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
from datetime import datetime
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"):
memory_type = "temporary"
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 == "temporary":
if memory_type in NEEDS_EXPIRY:
if expires_at:
exp_epoch = memory_client.parse_expires_from_text(expires_at)
if not exp_epoch:
@ -323,53 +337,34 @@ def _tool_memory_suggest(scope, kind, content, memory_type="temporary", expires_
"kind": kind,
"content": content,
"source": "bot-suggest",
"status": "candidate",
"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)
prev_type = last_suggest_result.get("type")
if result and result.get("duplicate"):
ex_status = result.get("existing_status", "?")
ex_type = result.get("existing_memory_type", "")
ex_exp = result.get("existing_expires_at")
ex_id = result.get("existing_id")
if ex_status == "candidate":
if prev_type != "new_candidate":
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."
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"):
last_suggest_result = {"type": "new_candidate", "candidate_id": None}
type_label = "temporaer" if memory_type == "temporary" else "dauerhaft"
return f"Vorschlag gespeichert als Kandidat ({type_label})."
return "Konnte Vorschlag nicht speichern."
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 _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_smart_mail_digest": lambda hours=24: _tool_get_smart_mail_digest(hours=hours),
"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_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.
GEDAECHTNIS memory_suggest:
Du MUSST memory_suggest aufrufen wenn der User etwas sagt das spaeter nuetzlich ist.
Dabei IMMER memory_type angeben:
Du MUSST memory_suggest aufrufen wenn der User etwas Nuetzliches sagt.
memory_suggest speichert DIREKT. Duplikate werden automatisch erkannt. Widersprueche werden automatisch aufgeloest (altes Item wird ersetzt).
TEMPORARY (memory_type="temporary") hat ein Ablaufdatum:
- Reiseplaene ("fliege nach...", "bin naechste Woche in...")
- Termine ("morgen 14 Uhr Zahnarzt", "am Freitag Meeting")
- Kurzfristige Aufenthalte ("bin bis Mittwoch in Berlin")
- Einmalige Vorhaben ("will diese Woche den Server migrieren")
Bei temporary: expires_at mit dem relevanten Zeitausdruck angeben (z.B. "naechste Woche", "morgen", "am Freitag").
MEMORY_TYPE immer angeben:
- "preference" Vorlieben ("Lieblingskaffee ist Flat White", "bevorzuge dunkles Theme")
- "relationship" Beziehungen/Rollen ("Ali ist Ansprechpartner fuer Wohnung")
- "fact" Stabile Fakten ("Server IP geaendert", "Jarvis aktiv seit...")
- "plan" Konkrete Vorhaben mit Zeitbezug ("Reise nach Frankfurt am 18.03.")
- "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:
- Persoenliche Praeferenzen ("ich bevorzuge...", "nenn mich...")
- Rollen/Beziehungen ("Ali ist mein Ansprechpartner fuer...")
- Stabile Infrastruktur-Fakten ("neuer Server heisst...", "IP geaendert auf...")
- Projektstatus ("Jarvis ist jetzt aktiv")
- Wiederkehrende Regeln ("API-Kosten monatlich beobachten")
CONFIDENCE immer angeben:
- "high" Klare Aussage ("Mein Lieblingskaffee ist Flat White")
- "medium" Wahrscheinlich aber nicht 100% ("Ali kuemmert sich wohl darum")
- "low" Sehr vage ("vielleicht aendere ich das noch")
Im Zweifel: lieber temporary als permanent.
memory_suggest speichert DIREKT kein Bestaetigen noetig. Das System erkennt Duplikate automatisch. Bei Duplikaten sag kurz "Weiss ich schon." und beantworte die Frage.
NICHT speichern: Passwoerter, Tokens, Smalltalk, Hoeflichkeiten, reine Fragen.
REGELN:
- Bei plan/temporary: IMMER expires_at mit Zeitausdruck angeben ("naechste Woche", "morgen")
- 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:
- "Was haben wir besprochen?" session_summary OHNE topic
@ -303,17 +305,18 @@ TOOLS = [
"type": "function",
"function": {
"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": {
"type": "object",
"properties": {
"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"},
"content": {"type": "string", "description": "Der Fakt (kurz, 3. Person, z.B. 'Fliegt naechste Woche nach Frankfurt')"},
"memory_type": {"type": "string", "enum": ["temporary", "permanent"], "description": "temporary=zeitlich begrenzt (Reisen, Termine), permanent=dauerhaft (Praeferenzen, Rollen, Regeln)"},
"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."},
"kind": {"type": "string", "enum": ["fact", "preference", "rule", "note"], "description": "Grobe Kategorie"},
"content": {"type": "string", "description": "Der Fakt (kurz, 3. Person, z.B. 'Lieblingskaffee ist Flat White')"},
"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"],
"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:
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)
except Exception:
memory_block = ""

View file

@ -248,7 +248,7 @@ def delete_candidate(item_id: int) -> bool:
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})
if not result or "items" not in result:
return []
@ -263,26 +263,32 @@ def get_active_memory() -> list[dict]:
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:
"""Formatiert Memory-Items als Text-Block fuer den System-Prompt."""
if not items:
return ""
permanent = [i for i in items if i.get("memory_type") != "temporary"]
temporary = [i for i in items if i.get("memory_type") == "temporary"]
lines = ["", "=== GEDAECHTNIS ==="]
if permanent:
lines.append("-- Dauerhaft --")
for item in permanent:
lines.append(f"[{item['scope']}/{item['kind']}] {item['content']}")
if temporary:
lines.append("-- Temporaer --")
for item in temporary:
exp = item.get("expires_at")
exp_str = ""
if exp:
exp_str = " (bis " + datetime.fromtimestamp(exp).strftime("%d.%m.%Y") + ")"
lines.append(f"[{item['scope']}/{item['kind']}] {item['content']}{exp_str}")
TYPE_ICON = {"fact": "📌", "preference": "", "relationship": "👤", "plan": "📅", "temporary": "🕒", "uncertain": ""}
lines = ["", "=== GEDAECHTNIS (relevante Fakten) ==="]
for item in items:
mtype = item.get("memory_type", "fact")
icon = TYPE_ICON.get(mtype, "")
conf = item.get("confidence", "high")
exp = item.get("expires_at")
parts = [f"{icon} [{mtype}]"]
if conf != "high":
parts.append(f"({conf})")
parts.append(item["content"])
if exp:
parts.append(f"(bis {datetime.fromtimestamp(exp).strftime('%d.%m.%Y')})")
lines.append(" ".join(parts))
lines.append("=== ENDE GEDAECHTNIS ===")
return "\n".join(lines)

View file

@ -322,22 +322,30 @@ async def cmd_memory(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if not items:
await update.message.reply_text("Kein Gedaechtnis vorhanden.")
return
permanent = [i for i in items if i.get("memory_type") != "temporary"]
temporary = [i for i in items if i.get("memory_type") == "temporary"]
TYPE_ICON = {"fact": "📌", "preference": "", "relationship": "👤", "plan": "📅", "temporary": "🕒", "uncertain": ""}
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"]
if permanent:
lines.append(f"📌 Dauerhaft ({len(permanent)}):")
for i in permanent:
lines.append(f"{i['content'][:100]}")
if temporary:
lines.append(f"\n🕒 Temporaer ({len(temporary)}):")
for i in temporary:
for mtype in ("fact", "preference", "relationship", "plan", "temporary", "uncertain"):
group = groups.get(mtype, [])
if not group:
continue
icon = TYPE_ICON.get(mtype, "")
lines.append(f"{icon} {mtype.upper()} ({len(group)}):")
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_str = ""
if exp:
from datetime import datetime
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)
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)
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)
answer = llm.ask_with_tools(text, handlers, session_id=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...")
try:
context.last_suggest_result = {"type": None}
context.set_source_type("telegram_text")
handlers = context.get_tool_handlers(session_id=session_id)
answer = llm.ask_with_tools(text, handlers, session_id=session_id)
if session_id: