This commit is contained in:
+5
-1
@@ -34,12 +34,13 @@ from routers import (
|
||||
models,
|
||||
routing,
|
||||
system,
|
||||
sysadmin,
|
||||
voice,
|
||||
wissen,
|
||||
zeitmaschine,
|
||||
)
|
||||
from routers import reminders as reminders_router
|
||||
from services import ketten_digest, metrics_history, reminders, sentry, warmer
|
||||
from services import ketten_digest, metrics_history, reminders, sentry, warmer, sysadmin_report
|
||||
from starlette.requests import Request
|
||||
|
||||
# Zentrales Logging — Level via MC_LOG_LEVEL (INFO default). Eine Konfiguration
|
||||
@@ -65,6 +66,8 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
if sentry.ENABLED:
|
||||
tasks.append(asyncio.create_task(sentry.sentry_loop()))
|
||||
tasks.append(asyncio.create_task(reminders.reminders_loop()))
|
||||
# Einmal am Tag (08:00) Telegram Report als Sysadmin
|
||||
tasks.append(asyncio.create_task(sysadmin_report.report_loop()))
|
||||
if ketten_digest.ENABLED:
|
||||
# Werkstatt-Status aufs Handy: Fragen sofort, Meilensteine sofort, sonst 60-min-Puls.
|
||||
tasks.append(asyncio.create_task(ketten_digest.digest_loop()))
|
||||
@@ -109,6 +112,7 @@ app.include_router(health.router)
|
||||
app.include_router(models.router)
|
||||
app.include_router(routing.router)
|
||||
app.include_router(system.router)
|
||||
app.include_router(sysadmin.router)
|
||||
app.include_router(connect.router)
|
||||
app.include_router(agent.router)
|
||||
app.include_router(
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
from fastapi import APIRouter
|
||||
from services import sysadmin_report
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
@router.post("/sysadmin/report/trigger")
|
||||
async def trigger_sysadmin_report() -> dict:
|
||||
"""Stößt den Sysadmin-Telegram-Report sofort manuell an."""
|
||||
success = await sysadmin_report.generate_and_send_report()
|
||||
return {"ok": success, "message": "Report wurde angestoßen und via Telegram versendet." if success else "Fehler beim Versenden des Reports."}
|
||||
@@ -0,0 +1,126 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user