This commit is contained in:
+5
-1
@@ -34,12 +34,13 @@ from routers import (
|
|||||||
models,
|
models,
|
||||||
routing,
|
routing,
|
||||||
system,
|
system,
|
||||||
|
sysadmin,
|
||||||
voice,
|
voice,
|
||||||
wissen,
|
wissen,
|
||||||
zeitmaschine,
|
zeitmaschine,
|
||||||
)
|
)
|
||||||
from routers import reminders as reminders_router
|
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
|
from starlette.requests import Request
|
||||||
|
|
||||||
# Zentrales Logging — Level via MC_LOG_LEVEL (INFO default). Eine Konfiguration
|
# 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:
|
if sentry.ENABLED:
|
||||||
tasks.append(asyncio.create_task(sentry.sentry_loop()))
|
tasks.append(asyncio.create_task(sentry.sentry_loop()))
|
||||||
tasks.append(asyncio.create_task(reminders.reminders_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:
|
if ketten_digest.ENABLED:
|
||||||
# Werkstatt-Status aufs Handy: Fragen sofort, Meilensteine sofort, sonst 60-min-Puls.
|
# Werkstatt-Status aufs Handy: Fragen sofort, Meilensteine sofort, sonst 60-min-Puls.
|
||||||
tasks.append(asyncio.create_task(ketten_digest.digest_loop()))
|
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(models.router)
|
||||||
app.include_router(routing.router)
|
app.include_router(routing.router)
|
||||||
app.include_router(system.router)
|
app.include_router(system.router)
|
||||||
|
app.include_router(sysadmin.router)
|
||||||
app.include_router(connect.router)
|
app.include_router(connect.router)
|
||||||
app.include_router(agent.router)
|
app.include_router(agent.router)
|
||||||
app.include_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