diff --git a/homelab-ai-bot/llm.py b/homelab-ai-bot/llm.py index 11d213e3..3a450d20 100644 --- a/homelab-ai-bot/llm.py +++ b/homelab-ai-bot/llm.py @@ -48,7 +48,7 @@ PERMANENT (memory_type="permanent") — bleibt dauerhaft: - Wiederkehrende Regeln ("API-Kosten monatlich beobachten") Im Zweifel: lieber temporary als permanent. -IMMER memory_suggest aufrufen, auch wenn der User denselben Fakt wiederholt. Das System erkennt Duplikate automatisch und gibt den korrekten Status zurueck. Du sagst dann genau das was das Tool zurueckgibt. +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. SESSION-RUECKBLICK: diff --git a/homelab-ai-bot/telegram_bot.py b/homelab-ai-bot/telegram_bot.py index a5339a81..dc0dbbca 100644 --- a/homelab-ai-bot/telegram_bot.py +++ b/homelab-ai-bot/telegram_bot.py @@ -37,9 +37,9 @@ def _release_lock(): except OSError: pass -from telegram import BotCommand, Update, ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardMarkup, InlineKeyboardButton +from telegram import BotCommand, Update, ReplyKeyboardMarkup, KeyboardButton from telegram.ext import ( - Application, CommandHandler, MessageHandler, CallbackQueryHandler, filters, ContextTypes, + Application, CommandHandler, MessageHandler, filters, ContextTypes, ) BOT_COMMANDS = [ @@ -52,7 +52,7 @@ BOT_COMMANDS = [ BotCommand("report", "Tagesbericht"), BotCommand("check", "Monitoring-Check"), BotCommand("feeds", "Feed-Status & Artikel heute"), - BotCommand("memory", "Offene Memory-Kandidaten"), + BotCommand("memory", "Gedaechtnis anzeigen"), BotCommand("start", "Hilfe anzeigen"), ] @@ -122,7 +122,7 @@ async def cmd_start(update: Update, ctx: ContextTypes.DEFAULT_TYPE): "/report — Tagesbericht\n" "/check — Monitoring-Check\n" "/feeds — Feed-Status & Artikel\n" - "/memory — Offene Memory-Kandidaten\n\n" + "/memory — Gedaechtnis anzeigen\n\n" "Oder einfach eine Frage stellen!", reply_markup=KEYBOARD, ) @@ -313,75 +313,35 @@ def _find_matching_item(user_text: str, items: list[dict]) -> dict | None: return best -def _memory_buttons(item_id: int) -> InlineKeyboardMarkup: - return InlineKeyboardMarkup([[ - InlineKeyboardButton("🕒 Temporär", callback_data=f"mem_temp:{item_id}"), - InlineKeyboardButton("📌 Dauerhaft", callback_data=f"mem_perm:{item_id}"), - InlineKeyboardButton("❌ Verwerfen", callback_data=f"mem_delete:{item_id}"), - ]]) - - -def _format_candidate(item: dict) -> str: - mtype = item.get("memory_type") or "?" - type_icon = "🕒" if mtype == "temporary" else "📌" if mtype == "permanent" else "❓" - line = f"{type_icon} [{item['scope']}/{item['kind']}] {mtype}\n{item['content']}" - exp = item.get("expires_at") - if exp: - from datetime import datetime - line += f"\n⏰ Ablauf: {datetime.fromtimestamp(exp).strftime('%d.%m.%Y %H:%M')}" - return line async def cmd_memory(update: Update, ctx: ContextTypes.DEFAULT_TYPE): if not _authorized(update): return - candidates = memory_client.get_candidates() - if not candidates: - await update.message.reply_text("Keine offenen Kandidaten.") + items = memory_client.get_active_memory() + if not items: + await update.message.reply_text("Kein Gedaechtnis vorhanden.") return - for item in candidates: - text = _format_candidate(item) - await update.message.reply_text(text, reply_markup=_memory_buttons(item["id"])) + 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 = [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: + 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}") + text = "\n".join(lines) + await update.message.reply_text(text[:4000], reply_markup=KEYBOARD) -async def handle_memory_callback(update: Update, ctx: ContextTypes.DEFAULT_TYPE): - query = update.callback_query - await query.answer() - data = query.data or "" - content_preview = query.message.text.split("\n", 2)[-1][:120] if query.message.text else "" - - if data.startswith("mem_temp:"): - item_id = int(data.split(":")[1]) - candidates = memory_client.get_candidates() - item = next((c for c in candidates if c["id"] == item_id), None) - exp = None - if item: - exp = item.get("expires_at") or memory_client.parse_expires_from_text(item.get("content", "")) - if not exp: - exp = memory_client.default_expires() - ok = memory_client.activate_candidate(item_id, memory_type="temporary", expires_at=exp) - if ok: - from datetime import datetime - exp_str = datetime.fromtimestamp(exp).strftime("%d.%m.%Y") - await query.edit_message_text(f"🕒 Temporär gespeichert (bis {exp_str}): {content_preview}") - else: - await query.edit_message_text("Fehler beim Speichern.") - - elif data.startswith("mem_perm:"): - item_id = int(data.split(":")[1]) - ok = memory_client.activate_candidate(item_id, memory_type="permanent") - if ok: - await query.edit_message_text(f"📌 Dauerhaft gespeichert: {content_preview}") - else: - await query.edit_message_text("Fehler beim Speichern.") - - elif data.startswith("mem_delete:"): - item_id = int(data.split(":")[1]) - ok = memory_client.delete_candidate(item_id) - if ok: - await query.edit_message_text("🗑 Verworfen.") - else: - await query.edit_message_text("Fehler beim Loeschen.") async def handle_voice(update: Update, ctx: ContextTypes.DEFAULT_TYPE): @@ -455,8 +415,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, "candidate_id": None} - candidates_before = {c["id"] for c in memory_client.get_candidates()} + context.last_suggest_result = {"type": None} handlers = context.get_tool_handlers(session_id=session_id) answer = llm.ask_with_tools(text, handlers, session_id=session_id) if session_id: @@ -464,74 +423,9 @@ async def handle_message(update: Update, ctx: ContextTypes.DEFAULT_TYPE): memory_client.log_message(session_id, "assistant", answer) suggest = context.last_suggest_result - log.info("suggest_result: type=%s candidate_id=%s", suggest.get("type"), suggest.get("candidate_id")) + log.info("suggest_result: type=%s", suggest.get("type")) - sent = False - - if suggest["type"] == "existing_candidate" and suggest["candidate_id"]: - candidates = memory_client.get_candidates() - item = next((c for c in candidates if c["id"] == suggest["candidate_id"]), None) - if item: - await update.message.reply_text( - answer[:4000] + "\n\n" + _format_candidate(item), - reply_markup=_memory_buttons(item["id"]), - ) - sent = True - - elif suggest["type"] in ("active_temporary", "active_permanent", "active_other"): - from datetime import datetime as _dt - active_items = memory_client.get_active_memory() - matched = _find_matching_item(text, active_items) - if matched: - mtype = matched.get("memory_type", "") - exp = matched.get("expires_at") - if mtype == "temporary" and exp: - status_msg = f"\n\n🕒 Schon temporär gespeichert bis {_dt.fromtimestamp(exp).strftime('%d.%m.%Y')}." - elif mtype == "permanent": - status_msg = "\n\n📌 Schon dauerhaft gespeichert." - else: - status_msg = "\n\n✅ Bereits gespeichert." - await update.message.reply_text(answer[:3900] + status_msg, reply_markup=KEYBOARD) - sent = True - - elif suggest["type"] == "new_candidate": - candidates_after = memory_client.get_candidates() - new_candidates = [c for c in candidates_after if c["id"] not in candidates_before] - if new_candidates: - c = new_candidates[0] - type_icon = "🕒" if c.get("memory_type") == "temporary" else "📌" if c.get("memory_type") == "permanent" else "📝" - await update.message.reply_text( - answer[:4000] + "\n\n" + type_icon + " " + c["content"], - reply_markup=_memory_buttons(c["id"]), - ) - sent = True - - if not sent and suggest["type"] is None: - from datetime import datetime as _dt - matched_active = _find_matching_item(text, memory_client.get_active_memory()) - if matched_active: - mtype = matched_active.get("memory_type", "") - exp = matched_active.get("expires_at") - if mtype == "temporary" and exp: - status_msg = f"\n\n🕒 Schon temporär gespeichert bis {_dt.fromtimestamp(exp).strftime('%d.%m.%Y')}." - elif mtype == "permanent": - status_msg = "\n\n📌 Schon dauerhaft gespeichert." - else: - status_msg = "\n\n✅ Bereits gespeichert." - await update.message.reply_text(answer[:3900] + status_msg, reply_markup=KEYBOARD) - sent = True - else: - matched_cand = _find_matching_item(text, memory_client.get_candidates()) - if matched_cand: - log.info("Fallback-Match: Kandidat ID=%s", matched_cand["id"]) - await update.message.reply_text( - answer[:4000] + "\n\n" + _format_candidate(matched_cand), - reply_markup=_memory_buttons(matched_cand["id"]), - ) - sent = True - - if not sent: - await update.message.reply_text(answer[:4000], reply_markup=KEYBOARD) + await update.message.reply_text(answer[:4000], reply_markup=KEYBOARD) except Exception as e: log.exception("Fehler bei Freitext") await update.message.reply_text(f"Fehler: {e}") @@ -561,7 +455,6 @@ def main(): app.add_handler(CommandHandler("check", cmd_check)) app.add_handler(CommandHandler("feeds", cmd_feeds)) app.add_handler(CommandHandler("memory", cmd_memory)) - app.add_handler(CallbackQueryHandler(handle_memory_callback, pattern=r"^mem_")) app.add_handler(MessageHandler(filters.VOICE, handle_voice)) app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))