129 lines
4.8 KiB
Python
129 lines
4.8 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 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: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": 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)
|