From be843543602ac901582c79abc521864781c5e44b Mon Sep 17 00:00:00 2001 From: Auto-Sync Date: Thu, 9 Apr 2026 17:15:36 +0200 Subject: [PATCH] Auto-Sync: 2026-04-09 17:15 --- arakava-news/STATE.md | 12 +- homelab-ai-bot/model_library_routes.py | 345 +++++++++++++++++++++++++ infrastructure/STATE.md | 2 +- smart-home/STATE.md | 2 +- 4 files changed, 353 insertions(+), 8 deletions(-) create mode 100644 homelab-ai-bot/model_library_routes.py diff --git a/arakava-news/STATE.md b/arakava-news/STATE.md index 06f267ca..64ba2cb7 100644 --- a/arakava-news/STATE.md +++ b/arakava-news/STATE.md @@ -1,5 +1,5 @@ # Arakava News — Live State -> Auto-generiert: 2026-04-09 16:45 +> Auto-generiert: 2026-04-09 17:15 ## Service Status | Service | CT | Status | @@ -8,11 +8,11 @@ | WordPress Docker | 101 | running | ## Letzte Feed-Aktivität (Top 5) -Antispiegel: 2026-04-09 14:30:42 - PAZ: 2026-04-09 14:30:21 - Junge Freiheit: 2026-04-09 14:00:45 - Dr. Bines Substack: 2026-04-09 14:00:00 - Tichys Einblick: 2026-04-09 13:30:24 +Apollo News: 2026-04-09 15:00:33 + Photon.info (KI-Analyse): 2026-04-09 14:46:35 + Rubikon.news: 2026-04-09 14:45:29 + Norbert Häring: 2026-04-09 14:45:29 + Antispiegel: 2026-04-09 14:30:42 ## Fehler (letzte 24h) - Fehler gesamt: 0 diff --git a/homelab-ai-bot/model_library_routes.py b/homelab-ai-bot/model_library_routes.py new file mode 100644 index 00000000..195b109a --- /dev/null +++ b/homelab-ai-bot/model_library_routes.py @@ -0,0 +1,345 @@ +"""Model Library Integration — Telegram-Buttons, Befehle, Watcher-Webhook. + +Wird von telegram_bot.py eingebunden: + import model_library_routes + model_library_routes.register(app, bot_token, chat_id) +""" + +import json +import logging +import threading +import time + +import requests +from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update +from telegram.ext import ContextTypes + +log = logging.getLogger(__name__) + +MODEL_LIBRARY_URL = "http://100.116.108.62:8090" +_CHAT_ID: int | None = None +_APP = None +_TG_TOKEN: str | None = None +_PENDING_FILE = "/var/cache/model-library-pending.json" + +# --- Persistence for pending notifications --- + + +def _load_pending() -> list[dict]: + try: + with open(_PENDING_FILE) as f: + return json.load(f) + except (FileNotFoundError, json.JSONDecodeError): + return [] + + +def _save_pending(items: list[dict]): + with open(_PENDING_FILE, "w") as f: + json.dump(items, f, ensure_ascii=False, indent=2) + + +def _remove_pending(repo_id: str, filename: str): + items = [ + m for m in _load_pending() + if not (m.get("repo_id") == repo_id and m.get("filename") == filename) + ] + _save_pending(items) + + +# --- Telegram handlers --- + + +async def cmd_modelle(update: Update, ctx: ContextTypes.DEFAULT_TYPE): + """Zeigt heruntergeladene Modelle aus dem Katalog.""" + try: + r = requests.get(f"{MODEL_LIBRARY_URL}/api/catalog", timeout=10) + r.raise_for_status() + catalog = r.json() + except Exception as e: + await update.message.reply_text(f"Fehler beim Abruf: {e}") + return + + models = catalog.get("models", []) + if not models: + await update.message.reply_text("📂 Bibliothek ist leer.") + return + + try: + sr = requests.get(f"{MODEL_LIBRARY_URL}/api/storage", timeout=5) + storage = sr.json() + header = ( + f"💾 {storage['used_gb']:.0f} / {storage['total_gb']:.0f} GB " + f"({storage['percent_used']:.0f}%) belegt\n\n" + ) + except Exception: + header = "" + + cat_groups: dict[str, list] = {} + for m in models: + cat_groups.setdefault(m.get("category", "llm"), []).append(m) + + cat_icons = { + "llm": "🧠", "code": "💻", "vision": "👁", "embedding": "📐", + "audio": "🎵", "image-gen": "🎨", + } + + lines = [f"📚 *KI-Modell-Bibliothek* ({len(models)} Modelle)\n{header}"] + for cat, items in sorted(cat_groups.items()): + icon = cat_icons.get(cat, "📦") + lines.append(f"{icon} *{cat.upper()}*") + for m in sorted(items, key=lambda x: x["filename"]): + q = m.get("quant", "?") + sz = m.get("size_gb", 0) + lines.append(f" • `{m['filename']}` ({q}, {sz:.1f} GB)") + lines.append("") + + await update.message.reply_text("\n".join(lines), parse_mode="Markdown") + + +async def cmd_watchlist(update: Update, ctx: ContextTypes.DEFAULT_TYPE): + """Zeigt die aktuelle Model-Watchlist.""" + try: + r = requests.get(f"{MODEL_LIBRARY_URL}/api/watchlist", timeout=10) + r.raise_for_status() + data = r.json() + except Exception as e: + await update.message.reply_text(f"Fehler beim Abruf: {e}") + return + + authors = data.get("authors", []) + if not authors: + await update.message.reply_text("Watchlist ist leer.") + return + + lines = ["👀 *Model-Watchlist*\n"] + for a in authors: + name = a.get("name", "?") + comment = a.get("comment", "") + lines.append(f" • *{name}* — {comment}") + + await update.message.reply_text("\n".join(lines), parse_mode="Markdown") + + +def _resolve_callback(data: str) -> tuple[str, str] | None: + """Löst Callback-Data auf — direkt (repo|file) oder über Index (idx_N).""" + prefix = data[:9] + payload = data[9:] + + if payload.startswith("idx_"): + try: + idx = int(payload[4:]) + pending = _load_pending() + if 0 <= idx < len(pending): + m = pending[idx] + return m.get("repo_id"), m.get("filename") + except (ValueError, IndexError): + pass + return None + + if "|" in payload: + return tuple(payload.split("|", 1)) + return None + + +async def handle_model_callback(data: str, query) -> bool: + """Verarbeitet model_dl_* und model_ig_* Callbacks. Gibt True zurück wenn verarbeitet.""" + if data.startswith("model_dl_"): + resolved = _resolve_callback(data) + if not resolved: + await query.edit_message_text(query.message.text + "\n\n⚠️ Modell nicht mehr verfügbar") + return True + repo_id, filename = resolved + + await query.edit_message_text( + query.message.text + f"\n\n⏳ Download gestartet: `{filename}`", + parse_mode="Markdown", + ) + try: + r = requests.post( + f"{MODEL_LIBRARY_URL}/api/download", + json={"repo_id": repo_id, "filename": filename}, + timeout=15, + ) + result = r.json() + if result.get("status") == "started": + await query.edit_message_text( + query.message.text.split("\n\n⏳")[0] + + f"\n\n✅ Download läuft: `{filename}`", + parse_mode="Markdown", + ) + else: + err = result.get("error", "Unbekannter Fehler") + await query.edit_message_text( + query.message.text.split("\n\n⏳")[0] + + f"\n\n⚠️ {err}", + parse_mode="Markdown", + ) + except Exception as e: + log.exception("Model download trigger failed") + await query.edit_message_text( + query.message.text.split("\n\n⏳")[0] + + f"\n\n❌ Fehler: {e}", + parse_mode="Markdown", + ) + _remove_pending(repo_id, filename) + return True + + if data.startswith("model_ig_"): + resolved = _resolve_callback(data) + if not resolved: + await query.edit_message_text(query.message.text + "\n\n⚠️ Modell nicht mehr verfügbar") + return True + repo_id, filename = resolved + + try: + requests.post( + f"{MODEL_LIBRARY_URL}/api/ignore", + json={"repo_id": repo_id, "filename": filename}, + timeout=10, + ) + except Exception: + pass + await query.edit_message_text( + query.message.text + "\n\n🚫 Ignoriert" + ) + _remove_pending(repo_id, filename) + return True + + return False + + +# --- Webhook: Watcher meldet neue Modelle --- + + +def _notify_new_models_sync(models: list[dict]): + """Sendet Telegram-Nachrichten mit Inline-Buttons fuer jedes neue Modell. + + Nutzt die Telegram HTTP API direkt (nicht python-telegram-bot), + da wir in einem Worker-Thread ohne asyncio-Loop laufen. + """ + if not _TG_TOKEN or not _CHAT_ID: + log.warning("Bot-Token oder Chat-ID nicht konfiguriert") + return + + MAX_PER_BATCH = 10 + if len(models) > MAX_PER_BATCH: + log.warning("Benachrichtigungen begrenzt: %d -> %d", len(models), MAX_PER_BATCH) + models = models[:MAX_PER_BATCH] + + pending = _load_pending() + sent = 0 + + for m in models: + repo_id = m.get("repo_id", "?") + filename = m.get("filename", "?") + size_gb = m.get("size_gb", 0) + quant = m.get("quant", "?") + + already = any( + p.get("repo_id") == repo_id and p.get("filename") == filename + for p in pending + ) + if already: + continue + + cb_dl = f"model_dl_{repo_id}|{filename}" + cb_ig = f"model_ig_{repo_id}|{filename}" + + used_index = False + if len(cb_dl.encode()) > 64 or len(cb_ig.encode()) > 64: + pending.append(m) + idx = len(pending) - 1 + _save_pending(pending) + used_index = True + cb_dl = f"model_dl_idx_{idx}" + cb_ig = f"model_ig_idx_{idx}" + + safe_repo = repo_id.replace("_", "\\_") + text = ( + f"🆕 *Neues KI-Modell verfügbar*\n\n" + f"📦 `{filename}`\n" + f"🏷 Repo: {safe_repo}\n" + f"📏 {size_gb:.1f} GB | Quant: {quant}" + ) + + keyboard = { + "inline_keyboard": [[ + {"text": "✅ Herunterladen", "callback_data": cb_dl}, + {"text": "🚫 Ignorieren", "callback_data": cb_ig}, + ]] + } + + try: + resp = requests.post( + f"https://api.telegram.org/bot{_TG_TOKEN}/sendMessage", + json={ + "chat_id": _CHAT_ID, + "text": text, + "parse_mode": "Markdown", + "reply_markup": keyboard, + }, + timeout=15, + ) + if resp.ok: + log.info("Modell-Benachrichtigung gesendet: %s", filename) + sent += 1 + else: + log.error("Telegram API Fehler: %s %s", resp.status_code, resp.text[:200]) + except Exception: + log.exception("Fehler beim Senden der Modell-Benachrichtigung: %s", filename) + + if not used_index: + pending.append(m) + _save_pending(pending) + time.sleep(1.5) + + +def start_webhook_listener(): + """Startet einen kleinen Flask-Server (Port 8767) fuer Watcher-Webhooks.""" + from flask import Flask, request as freq, jsonify + + webhook_app = Flask("model-library-webhook") + webhook_app.logger.setLevel(logging.WARNING) + werkzeug_log = logging.getLogger("werkzeug") + werkzeug_log.setLevel(logging.WARNING) + + @webhook_app.route("/api/notify_new_models", methods=["POST"]) + def notify(): + data = freq.get_json(silent=True) + if not data: + return jsonify({"error": "no data"}), 400 + models = data.get("models", []) + if models: + threading.Thread( + target=_notify_new_models_sync, args=(models,), daemon=True + ).start() + return jsonify({"status": "ok", "queued": len(models)}) + + @webhook_app.route("/health", methods=["GET"]) + def health(): + return jsonify({"status": "ok"}) + + t = threading.Thread( + target=lambda: webhook_app.run(host="0.0.0.0", port=8767, use_reloader=False), + daemon=True, + ) + t.start() + log.info("Model-Library Webhook-Listener auf Port 8767 gestartet") + + +# --- Registration --- + + +def register(app, chat_id: int | None, token: str | None = None): + """Wird von telegram_bot.py aufgerufen.""" + global _CHAT_ID, _APP, _TG_TOKEN + _CHAT_ID = chat_id + _APP = app + _TG_TOKEN = token + + from telegram.ext import CommandHandler + app.add_handler(CommandHandler("modelle", cmd_modelle)) + app.add_handler(CommandHandler("watchlist", cmd_watchlist)) + + start_webhook_listener() + log.info("Model-Library Routes registriert (/modelle, /watchlist, Webhook :8767)") diff --git a/infrastructure/STATE.md b/infrastructure/STATE.md index 95389a34..a166b803 100644 --- a/infrastructure/STATE.md +++ b/infrastructure/STATE.md @@ -1,5 +1,5 @@ # Infrastruktur — Live State -> Auto-generiert: 2026-04-09 16:45 +> Auto-generiert: 2026-04-09 17:15 ## pve-hetzner Disk | Mount | Belegt | diff --git a/smart-home/STATE.md b/smart-home/STATE.md index 0168ed84..44100326 100644 --- a/smart-home/STATE.md +++ b/smart-home/STATE.md @@ -1,5 +1,5 @@ # Smart Home Muldenstein — Live State -> Auto-generiert: 2026-04-09 16:45 +> Auto-generiert: 2026-04-09 17:15 ## Backup-Status - Letztes Backup: 656MB, 2026-04-09 04:01