From e091f79f0a3b55a794f9bc97231f64eec38f2bce Mon Sep 17 00:00:00 2001 From: root Date: Sun, 15 Mar 2026 13:25:09 +0700 Subject: [PATCH] Memory: Fallback-Matching bei Duplikaten wenn LLM Tool nicht aufruft --- homelab-ai-bot/telegram_bot.py | 47 ++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/homelab-ai-bot/telegram_bot.py b/homelab-ai-bot/telegram_bot.py index 01fdad9e..df0c1162 100644 --- a/homelab-ai-bot/telegram_bot.py +++ b/homelab-ai-bot/telegram_bot.py @@ -286,6 +286,32 @@ async def cmd_feeds(update: Update, ctx: ContextTypes.DEFAULT_TYPE): except Exception as e: await update.message.reply_text(f"Fehler: {e}") +_STOP_WORDS = {"ich", "bin", "ist", "der", "die", "das", "ein", "eine", "und", "oder", + "in", "auf", "an", "fuer", "für", "von", "zu", "mit", "nach", "mein", + "meine", "meinem", "meinen", "hat", "habe", "wird", "will", "soll", + "nicht", "auch", "noch", "schon", "nur", "aber", "wenn", "weil", "dass"} + + +def _find_matching_candidate(user_text: str, candidates: list[dict]) -> dict | None: + """Findet den Kandidaten mit der besten Wort-Ueberlappung zum User-Text.""" + words = {w.lower().strip(".,!?") for w in user_text.split() if len(w) > 2} + words -= _STOP_WORDS + if not words: + return None + + best, best_score = None, 0 + for c in candidates: + content_words = {w.lower().strip(".,!?") for w in c["content"].split() if len(w) > 2} + content_words -= _STOP_WORDS + if not content_words: + continue + overlap = len(words & content_words) + score = overlap / max(len(words), 1) + if score > best_score and score >= 0.3: + best, best_score = c, score + return best + + def _memory_buttons(item_id: int) -> InlineKeyboardMarkup: return InlineKeyboardMarkup([[ InlineKeyboardButton("🕒 Temporär", callback_data=f"mem_temp:{item_id}"), @@ -395,6 +421,8 @@ async def handle_message(update: Update, ctx: ContextTypes.DEFAULT_TYPE): suggest = context.last_suggest_result log.info("suggest_result: type=%s candidate_id=%s", suggest.get("type"), suggest.get("candidate_id")) + 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) @@ -403,8 +431,7 @@ async def handle_message(update: Update, ctx: ContextTypes.DEFAULT_TYPE): answer[:4000] + "\n\n" + _format_candidate(item), reply_markup=_memory_buttons(item["id"]), ) - else: - await update.message.reply_text(answer[:4000], reply_markup=KEYBOARD) + sent = True elif suggest["type"] == "new_candidate": candidates_after = memory_client.get_candidates() @@ -416,9 +443,19 @@ async def handle_message(update: Update, ctx: ContextTypes.DEFAULT_TYPE): answer[:4000] + "\n\n" + type_icon + " " + c["content"], reply_markup=_memory_buttons(c["id"]), ) - else: - await update.message.reply_text(answer[:4000], reply_markup=KEYBOARD) - else: + sent = True + + if not sent and suggest["type"] is None: + matched = _find_matching_candidate(text, memory_client.get_candidates()) + if matched: + log.info("Fallback-Match: Kandidat ID=%s fuer User-Text", matched["id"]) + await update.message.reply_text( + answer[:4000] + "\n\n" + _format_candidate(matched), + reply_markup=_memory_buttons(matched["id"]), + ) + sent = True + + if not sent: await update.message.reply_text(answer[:4000], reply_markup=KEYBOARD) except Exception as e: log.exception("Fehler bei Freitext")