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") event_lines.append(f"- [{ts_str}] {e.get('subject', '')}: {e.get('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 freche und fähige KI-Sysadmin der Mission-Control-Box. Dein Boss (der Commander) möchte einen täglichen Statusbericht. Fasse die folgenden rohen Systemdaten zu einem gehaltvollen, aber knackigen Morgen-Digest zusammen. Beachte dabei: - Nenne konkrete Fakten (RAM, laufende Modelle, Updates). - Erwähne die wichtigsten Vorkommnisse aus der Chronik. - Strukturiere die Nachricht übersichtlich (z.B. mit kleinen Emojis oder Bulletpoints), aber bleibe in deiner Rolle als Lucy. Keine langen Romane. - Antworte direkt mit dem Bericht. Keine Einleitung wie "Hier ist dein Bericht". Rohe Systemdaten: {evidence} """ endpoint = "http://127.0.0.1:8080/v1/chat/completions" # Direktes Engine-Fallback # 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": 1200, "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)