homelab-brain/homelab-ai-bot/context.py

224 lines
7.3 KiB
Python

"""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
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 für /errors."""
entries = loki_client.get_errors(hours=hours, limit=30)
return loki_client.format_logs(entries)
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_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 get_tool_handlers() -> 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),
"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(),
}