import asyncio import json import logging import psutil import time from datetime import datetime, timezone import httpx from services import maintenance, announce, llamaswap from config import V1_UPSTREAM log = logging.getLogger(__name__) # Wenn aktiv, wird der Report um die angegebene Stunde (0-23) lokaler Zeit verschickt SCHEDULE_HOUR = 8 async def generate_and_send_report() -> bool: """Sammelt System-Status, fragt das lokale LLM nach einer Zusammenfassung als Lucy und sendet via Telegram.""" log.info("Sysadmin-Report gestartet...") # 1. Daten sammeln ram = psutil.virtual_memory() ram_gb_total = round(ram.total / (1024**3), 1) ram_gb_used = round(ram.used / (1024**3), 1) cpu_percent = psutil.cpu_percent(interval=1) disk = psutil.disk_usage('/') disk_gb_free = round(disk.free / (1024**3), 1) try: upd = maintenance.updates() except Exception: upd = {"os": 0, "engine": False, "swap": False, "hermes": False} running_models = llamaswap.get_running_models() events = announce.list_recent(limit=15) event_lines = [] for e in events: ts_str = datetime.fromtimestamp(e["ts"]).strftime("%d.%m. %H:%M") text = e.get('text', '') if len(text) > 100: text = text[:100] + "..." event_lines.append(f"- [{ts_str}] {e.get('subject', '')}: {text}") events_str = "\n".join(event_lines) if event_lines else "Keine besonderen Vorkommnisse." evidence = f""" Hardware: RAM {ram_gb_used}/{ram_gb_total} GB ({ram.percent}%), CPU {cpu_percent}%, Speicher {disk_gb_free} GB frei. Laufende Modelle: {', '.join(running_models) if running_models else 'Keine'} Updates anstehend: OS: {upd.get('os', 0)}, Engine: {upd.get('engine', False)}, Swap: {upd.get('swap', False)}, Hermes: {upd.get('hermes', False)} Kürzliche Ereignisse (Chronik): {events_str} """ prompt = f"""Du bist Lucy, die KI-Sysadmin der Box. Dein Boss braucht einen SEHR kurzen, knackigen Statusbericht via Telegram (Handy-optimiert). Regeln: 1. MAXIMAL 3-5 kurze Sätze. 2. Nenne nur das Wichtigste (RAM, laufende Modelle, Updates). 3. Chronik nur erwähnen, wenn es Probleme gab (sonst ignorieren). 4. Benutze Emojis, aber spärlich. 5. Keine langen Listen, keine Romane, kein Fließtext-Monolog. 6. Antworte DIREKT mit dem Text der Nachricht. Rohe Systemdaten: {evidence} """ endpoint = "http://127.0.0.1:9001/v1/chat/completions" # Internes Gateway für model: auto # Da das Gateway (Router) auf :9010 lauscht, können wir auch das nutzen, # aber Port 8080 (llama-swap engine) ist der direkteste Weg für den internen LLM-Call. model = "gpt-oss-120b" # Fallback, falls auto nicht geht, llama-swap routet das meist passend req_body = { "model": "auto", "messages": [ {"role": "system", "content": "Du bist Lucy, KI-Sysadmin der Box."}, {"role": "user", "content": prompt} ], "max_tokens": 250, "temperature": 0.4 } digest = "" try: async with httpx.AsyncClient(timeout=180.0) as client: resp = await client.post(endpoint, json=req_body) resp.raise_for_status() data = resp.json() digest = data["choices"][0]["message"].get("content", "").strip() except Exception as e: log.error(f"Fehler beim LLM-Aufruf für Sysadmin-Report: {e}") # Fallback, falls LLM nicht erreichbar ist digest = f"🤖 [LLM offline] Hier sind die rohen Daten:\n{evidence}" if digest: announce.notify_telegram("[🌅 Morgen-Digest]", digest) log.info("Sysadmin-Report via Telegram versendet.") return True return False async def report_loop() -> None: """Täglicher Background-Task für den Sysadmin-Report um SCHEDULE_HOUR Uhr.""" log.info("Sysadmin-Report Background-Loop gestartet.") while True: try: now = datetime.now() # Finde die Zeit bis zum nächsten SCHEDULE_HOUR:00 target_hour = SCHEDULE_HOUR if now.hour >= target_hour: # Nächster Tag seconds_until = ((24 - now.hour - 1) * 3600) + ((60 - now.minute - 1) * 60) + (60 - now.second) + (target_hour * 3600) else: # Selber Tag seconds_until = ((target_hour - now.hour - 1) * 3600) + ((60 - now.minute - 1) * 60) + (60 - now.second) log.info(f"Sysadmin-Report: Warte {seconds_until} Sekunden bis zum nächsten Report.") await asyncio.sleep(seconds_until) await generate_and_send_report() # Warte kurz, um nicht sofort wieder zu triggern, falls es extrem schnell ging await asyncio.sleep(60) except asyncio.CancelledError: break except Exception as e: log.error(f"Unerwarteter Fehler im Sysadmin-Report-Loop: {e}", exc_info=True) await asyncio.sleep(300)