From f9ea49013a1517090ca1a53bc2fd293690766a1c Mon Sep 17 00:00:00 2001 From: root Date: Sun, 15 Mar 2026 12:55:04 +0700 Subject: [PATCH] Hausmeister: Inline-Buttons fuer Memory-Kandidaten + /memory Befehl --- homelab-ai-bot/memory_client.py | 26 ++++++++++++++ homelab-ai-bot/telegram_bot.py | 62 +++++++++++++++++++++++++++++++-- 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/homelab-ai-bot/memory_client.py b/homelab-ai-bot/memory_client.py index 12203d45..a26e75e6 100644 --- a/homelab-ai-bot/memory_client.py +++ b/homelab-ai-bot/memory_client.py @@ -155,6 +155,32 @@ def log_message(session_id: str, role: str, content: str, source: str = None, me _post(f"/sessions/{session_id}/messages", data) +def get_candidates() -> list[dict]: + """Holt alle offenen Memory-Kandidaten.""" + result = _get("/memory", {"status": "candidate", "limit": 20}) + if result and "items" in result: + return result["items"] + return [] + + +def activate_candidate(item_id: int) -> bool: + """Setzt einen Kandidaten auf aktiv.""" + result = _patch(f"/memory/{item_id}", {"status": "active"}) + return bool(result and result.get("ok")) + + +def delete_candidate(item_id: int) -> bool: + """Loescht einen Kandidaten.""" + _ensure_config() + if not _base_url: + return False + try: + r = requests.delete(f"{_base_url}/memory/{item_id}", headers=_headers(), timeout=5) + return r.ok + except Exception: + return False + + def get_active_memory() -> list[dict]: """Holt alle aktiven Memory-Items fuer den System-Prompt.""" result = _get("/memory", {"status": "active", "limit": 100}) diff --git a/homelab-ai-bot/telegram_bot.py b/homelab-ai-bot/telegram_bot.py index 59624cc5..7b93228e 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 +from telegram import BotCommand, Update, ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardMarkup, InlineKeyboardButton from telegram.ext import ( - Application, CommandHandler, MessageHandler, filters, ContextTypes, + Application, CommandHandler, MessageHandler, CallbackQueryHandler, filters, ContextTypes, ) BOT_COMMANDS = [ @@ -52,6 +52,7 @@ BOT_COMMANDS = [ BotCommand("report", "Tagesbericht"), BotCommand("check", "Monitoring-Check"), BotCommand("feeds", "Feed-Status & Artikel heute"), + BotCommand("memory", "Offene Memory-Kandidaten"), BotCommand("start", "Hilfe anzeigen"), ] @@ -284,6 +285,47 @@ async def cmd_feeds(update: Update, ctx: ContextTypes.DEFAULT_TYPE): except Exception as e: await update.message.reply_text(f"Fehler: {e}") +def _memory_buttons(item_id: int) -> InlineKeyboardMarkup: + return InlineKeyboardMarkup([[ + InlineKeyboardButton("āœ… Merken", callback_data=f"mem_activate:{item_id}"), + InlineKeyboardButton("āŒ Verwerfen", callback_data=f"mem_delete:{item_id}"), + ]]) + + +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.") + return + for item in candidates: + text = f"šŸ“ [{item['scope']}/{item['kind']}]\n{item['content']}" + await update.message.reply_text(text, reply_markup=_memory_buttons(item["id"])) + + +async def handle_memory_callback(update: Update, ctx: ContextTypes.DEFAULT_TYPE): + query = update.callback_query + await query.answer() + data = query.data or "" + + if data.startswith("mem_activate:"): + item_id = int(data.split(":")[1]) + ok = memory_client.activate_candidate(item_id) + if ok: + await query.edit_message_text("āœ… Aktiviert: " + query.message.text.split("\n", 1)[-1]) + else: + await query.edit_message_text("Fehler beim Aktivieren.") + + 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_message(update: Update, ctx: ContextTypes.DEFAULT_TYPE): """Button-Presses und Freitext-Fragen verarbeiten.""" if not _authorized(update): @@ -311,12 +353,24 @@ async def handle_message(update: Update, ctx: ContextTypes.DEFAULT_TYPE): await update.message.reply_text("šŸ¤” Denke nach...") try: + candidates_before = {c["id"] for c in memory_client.get_candidates()} handlers = context.get_tool_handlers(session_id=session_id) answer = llm.ask_with_tools(text, handlers, session_id=session_id) if session_id: memory_client.log_message(session_id, "user", text) memory_client.log_message(session_id, "assistant", answer) - await update.message.reply_text(answer[:4000], reply_markup=KEYBOARD) + + 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] + await update.message.reply_text( + answer[:4000] + "\n\nšŸ“ " + c["content"], + reply_markup=_memory_buttons(c["id"]), + ) + else: + 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}") @@ -345,6 +399,8 @@ def main(): app.add_handler(CommandHandler("report", cmd_report)) 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.TEXT & ~filters.COMMAND, handle_message)) async def post_init(application):