RAG v2: Klassifizierung (6 Typen, 3 Confidence), source_type, Auto-Supersede, /memory erweitert
This commit is contained in:
parent
91a6180b5a
commit
7ae6ac0e4c
4 changed files with 108 additions and 94 deletions
|
|
@ -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.",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = ""
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue