"""Intelligente Kontext-Sammlung für den Hausmeister-Bot. Entscheidet anhand der Frage welche Datenquellen abgefragt werden.""" import sys import os import re sys.path.insert(0, os.path.dirname(__file__)) from core import config, loki_client, proxmox_client, wordpress_client, prometheus_client from core import forgejo_client, seafile_client, pbs_client, mail_client def _load_config(): return config.parse_config() def _get_tokens(cfg): tokens = {} tn = cfg.raw.get("PVE_TOKEN_HETZNER_NAME", "") tv = cfg.raw.get("PVE_TOKEN_HETZNER_VALUE", "") if tn and tv: tokens["pve-hetzner"] = {"name": tn, "value": tv} return tokens def _get_passwords(cfg): pw_default = cfg.passwords.get("default", "") pw_hetzner = cfg.passwords.get("hetzner", pw_default) pws = {"default": pw_default, "pve-hetzner": pw_hetzner} for host in proxmox_client.PROXMOX_HOSTS: if host not in pws: pws[host] = pw_default return pws def gather_status() -> str: """Komplett-Status aller Container für /status.""" cfg = _load_config() containers = proxmox_client.get_all_containers( _get_passwords(cfg), _get_tokens(cfg) ) return proxmox_client.format_containers(containers) def gather_errors(hours: float = 2) -> str: """Aktuelle Fehler aus Loki — mit Anzahl + Beispiele.""" result = loki_client.count_errors(hours=hours) if "error" in result: return f"Loki-Fehler: {result['error']}" count = result["count"] per_host = result.get("per_host", {}) lines = [f"Fehler ({hours:.0f}h): {count} Einträge"] if per_host: top = sorted(per_host.items(), key=lambda x: x[1], reverse=True)[:5] lines.append("Top-Hosts:") for host, n in top: lines.append(f" {host}: {n}x") if count > 0: examples = loki_client.get_errors(hours=hours, limit=5) real = [e for e in examples if "error" not in e] if real: lines.append("Letzte Beispiele:") for e in real[:3]: lines.append(f" [{e.get('host','?')}] {e.get('line','')[:120]}") return "\n".join(lines) def gather_error_count(hours: float = 24) -> str: """Nur die Fehleranzahl aus Loki — für Zähl-Fragen.""" result = loki_client.count_errors(hours=hours) if "error" in result: return f"Loki-Fehler: {result['error']}" count = result["count"] per_host = result.get("per_host", {}) lines = [f"{count} Fehler-Einträge in den letzten {hours:.0f} Stunden"] if per_host: top = sorted(per_host.items(), key=lambda x: x[1], reverse=True)[:8] for host, n in top: lines.append(f" {host}: {n}x") return "\n".join(lines) def gather_container_status(query: str) -> str: """Status eines einzelnen Containers.""" cfg = _load_config() vmid = None name = None m = re.search(r'\b(\d{3})\b', query) if m: vmid = int(m.group(1)) else: name = query.strip() ct = config.get_container(cfg, vmid=vmid, name=name) if not ct: return f"Container nicht gefunden: {query}" host_ip = proxmox_client.PROXMOX_HOSTS.get(ct.host) if not host_ip: return f"Host nicht erreichbar: {ct.host}" token = _get_tokens(cfg).get(ct.host, {}) pw = _get_passwords(cfg).get(ct.host, "") try: client = proxmox_client.ProxmoxClient( host_ip, password=pw, token_name=token.get("name", ""), token_value=token.get("value", ""), ) status = client.get_container_status(ct.vmid) except Exception as e: return f"Proxmox-Fehler: {e}" mem_mb = status.get("mem", 0) // (1024 * 1024) maxmem_mb = status.get("maxmem", 0) // (1024 * 1024) uptime_h = status.get("uptime", 0) // 3600 return ( f"CT {ct.vmid} — {ct.name}\n" f"Host: {ct.host}\n" f"Status: {status.get('status', '?')}\n" f"RAM: {mem_mb}/{maxmem_mb} MB\n" f"CPU: {status.get('cpus', '?')} Kerne\n" f"Uptime: {uptime_h}h\n" f"Tailscale: {ct.tailscale_ip or '—'}\n" f"Dienste: {ct.services}" ) def gather_logs(container: str, hours: float = 1) -> str: """Logs eines Containers aus Loki.""" entries = loki_client.query_logs( f'{{host="{container}"}}', hours=hours, limit=20 ) return loki_client.format_logs(entries) def gather_health(container: str) -> str: """Health-Check eines Containers.""" health = loki_client.get_health(container, hours=24) status_emoji = {"healthy": "✅", "warning": "⚠️", "critical": "🔴"}.get( health.get("status", ""), "❓" ) return ( f"{status_emoji} {health.get('host', container)}\n" f"Status: {health.get('status', '?')}\n" f"Fehler (24h): {health.get('errors_last_{hours}h', '?')}\n" f"Sendet Logs: {'ja' if health.get('sending_logs') else 'nein'}" ) def gather_silence() -> str: """Welche Hosts senden keine Logs?""" silent = loki_client.check_silence(minutes=35) if not silent: return "✅ Alle Hosts senden Logs." if silent and "error" in silent[0]: return f"Fehler: {silent[0]['error']}" lines = ["⚠️ Stille Hosts (keine Logs seit 35+ Min):\n"] for s in silent: lines.append(f" • {s['host']}") return "\n".join(lines) def _tool_get_server_metrics(host: str = None) -> str: if host: return prometheus_client.format_host_detail(host) return prometheus_client.format_overview() def _tool_get_server_warnings() -> str: warnings = prometheus_client.get_warnings() return "\n".join(warnings) if warnings else "Keine Warnungen — alle Werte normal." def _tool_get_wordpress_stats() -> str: cfg = _load_config() wordpress_client.init(cfg) return wordpress_client.format_overview(cfg) def _tool_create_issue(title: str, body: str = "") -> str: cfg = _load_config() forgejo_client.init(cfg) result = forgejo_client.create_issue(title, body) if "error" in result: return result["error"] return f"Issue #{result['number']} erstellt: {result['title']}" def _tool_close_issue(number: int) -> str: cfg = _load_config() forgejo_client.init(cfg) result = forgejo_client.close_issue(int(number)) if "error" in result: return result["error"] return f"Issue #{result['number']} geschlossen: {result['title']}" def _tool_get_forgejo_status() -> str: cfg = _load_config() forgejo_client.init(cfg) return forgejo_client.format_overview() def _tool_get_seafile_status() -> str: cfg = _load_config() seafile_client.init(cfg) return seafile_client.format_overview() def _tool_get_backup_status() -> str: cfg = _load_config() pbs_client.init(cfg) return pbs_client.format_overview() def _tool_get_mail_summary() -> str: cfg = _load_config() mail_client.init(cfg) return mail_client.format_summary() def _tool_get_mail_count() -> str: cfg = _load_config() mail_client.init(cfg) counts = mail_client.get_mail_count() if "error" in counts: return f"Mail-Fehler: {counts['error']}" return f"E-Mails: {counts['total']} gesamt, {counts['unread']} ungelesen ({counts['account']})" def _tool_search_mail(query: str, days: int = 30) -> str: cfg = _load_config() mail_client.init(cfg) results = mail_client.search_mail(query, days=days) return mail_client.format_search_results(results) def _tool_get_todays_mails() -> str: cfg = _load_config() mail_client.init(cfg) mails = mail_client.get_todays_mails() if not mails: return "Heute keine Mails eingegangen." if "error" in mails[0]: return f"Mail-Fehler: {mails[0]['error']}" lines = [f"{len(mails)} Mail(s) heute:\n"] for r in mails: lines.append(f" {r['date_str']} | {r['from'][:35]}") lines.append(f" → {r['subject'][:70]}") return "\n".join(lines) def _tool_get_smart_mail_digest(hours: int = 24) -> str: cfg = _load_config() mail_client.init(cfg) api_key = cfg.api_keys.get("openrouter_key", "") digest = mail_client.get_smart_digest(hours=hours, api_key=api_key) return mail_client.format_smart_digest(digest) def _tool_get_feed_stats() -> str: cfg = _load_config() ct_109 = config.get_container(cfg, vmid=109) if not ct_109 or not ct_109.tailscale_ip: return "RSS Manager nicht erreichbar." import requests as _req try: r = _req.get(f"http://{ct_109.tailscale_ip}:8080/api/feed-stats", timeout=10) if not r.ok: return "RSS Manager API Fehler." stats = r.json() lines = [f"Artikel heute: {stats['today']}, gestern: {stats['yesterday']}"] for f in stats.get("feeds", []): if f["posts_today"] > 0: lines.append(f" {f['name']}: {f['posts_today']} heute") return "\n".join(lines) except Exception as e: return f"RSS Manager Fehler: {e}" def _tool_memory_read(scope=""): import memory_client items = memory_client.get_active_memory() if scope: items = [i for i in items if i.get("scope") == scope] if not items: return "Keine Memory-Eintraege gefunden." lines = [] for i in items: lines.append(f"[{i['scope']}/{i['kind']}] {i['content']}") return "\n".join(lines) def _tool_memory_suggest(scope, kind, content): import memory_client result = memory_client._post("/memory", { "scope": scope, "kind": kind, "content": content, "source": "bot-suggest", "status": "candidate", }) if result and result.get("duplicate"): return f"Bereits gespeichert (ID {result.get('existing_id')})." if result and result.get("ok"): return f"Vorschlag gespeichert als Kandidat (Fingerprint: {result.get('fingerprint', '?')[:12]}...)." return "Konnte Vorschlag nicht speichern." def _tool_session_search(query): import memory_client result = memory_client._get("/sessions/search", {"q": query, "limit": 20}) if not result or not result.get("results"): return f"Keine Ergebnisse fuer '{query}'." lines = [f"Suche '{query}': {result['count']} Treffer"] for r in result["results"][:10]: role = r.get("role", "?") content = (r.get("content") or "")[:150] lines.append(f" [{role}] {content}") return "\n".join(lines) def _tool_session_summary(session_id, topic=None): import memory_client return memory_client.get_session_summary(session_id, limit=20, topic=topic or None) def get_tool_handlers(session_id: str = None) -> dict: """Registry: Tool-Name -> Handler-Funktion. Wird von llm.ask_with_tools() genutzt.""" return { "get_all_containers": lambda: gather_status(), "get_container_detail": lambda query: gather_container_status(query), "get_errors": lambda hours=2: gather_errors(hours=hours), "count_errors": lambda hours=24: gather_error_count(hours=hours), "get_container_logs": lambda container, hours=1: gather_logs(container, hours=hours), "get_silent_hosts": lambda: gather_silence(), "get_server_metrics": lambda host=None: _tool_get_server_metrics(host), "get_server_warnings": lambda: _tool_get_server_warnings(), "get_wordpress_stats": lambda: _tool_get_wordpress_stats(), "get_feed_stats": lambda: _tool_get_feed_stats(), "get_forgejo_status": lambda: _tool_get_forgejo_status(), "create_issue": lambda title, body="": _tool_create_issue(title, body), "close_issue": lambda number: _tool_close_issue(number), "get_seafile_status": lambda: _tool_get_seafile_status(), "get_backup_status": lambda: _tool_get_backup_status(), "get_mail_summary": lambda: _tool_get_mail_summary(), "get_mail_count": lambda: _tool_get_mail_count(), "search_mail": lambda query, days=30: _tool_search_mail(query, days=days), "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: _tool_memory_suggest(scope, kind, content), "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.", }