Files
mission-control-v2/backend/services/sysadmin_report.py
T
Hitonabi 45f799c57d
Ampel / ampel (push) Failing after 22s
feat: implement daily Telegram sysadmin report
2026-08-07 11:12:06 +02:00

127 lines
5.0 KiB
Python

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)