"""Wie selbstständig Lucy handelt — je Aktionsart (Ausbauplan Welle 3, Punkt 14, seit 25.09.2026). User-Entscheid 25.09.2026 „Mehr Freiheit“: Lucy untersucht und erklärt von selbst und startet Dienste selbst neu; sie darf zusätzlich Sicherheitsupdates einspielen und bei Rot auf den Snapshot zurückgehen — mit Meldung danach. Andere Updates, das Aufräumen, Modell-Tests und den Neustart des Proxmox-Hosts gibt es erst nach dem Ja des Commanders. Je Aktionsart umstellbar (Einstellungen → Lucy): lesen nur lesen und erklären: Sie handelt nicht und fragt nicht — außer der Commander bittet sie ausdrücklich fragen erst fragen: eine Rückfrage (services/rueckfragen.py); sein „ja“ löst genau diese Aktion aus selbst selbst handeln, Meldung danach (gehandelt()) Bittet der Commander Lucy im Gespräch ausdrücklich um etwas, ist das sein Ja. Die Stufen gelten für alles, was sie aus eigenem Antrieb tut — auch in geplanten Läufen und wenn sie einen roten Hinweis untersucht. Gelesen werden sie von Lucys Werkzeugen (mcp/orchestrator_werkzeuge.py) und beim Untersuchen roter Hinweise (services/ermittlung.py). """ import json import logging import threading from dataclasses import dataclass from pathlib import Path from kern.einstellungen import einstellungen log = logging.getLogger(__name__) STUFEN = {"lesen": "Nur lesen und erklären", "fragen": "Erst fragen", "selbst": "Selbst handeln, Meldung danach"} STUFE_KURZ = {"lesen": "nur lesen", "fragen": "erst fragen", "selbst": "selbst handeln"} BETREFF_GEHANDELT = "[Lucy selbst]" @dataclass(frozen=True) class Art: id: str name: str text: str standard: str ARTEN: tuple[Art, ...] = ( Art("ermitteln", "Rote Hinweise untersuchen", "Lucy liest Protokolle und Messwerte und schickt dir Ursache und Lösungsweg. Selbst: sobald ein Hinweis rot " "wird. Erst fragen: nach deinem Ja. Nur lesen: wenn du sie fragst.", "selbst"), Art("neustart", "Dienste neu starten", "Dienste der KI-Box (Hermes, Motor, Steuerpult …), die hängen oder abgestürzt sind. Bewusst gestoppte startet " "sie nie von selbst.", "selbst"), Art("sicherheit", "Sicherheitsupdates einspielen", "Nur Pakete, die eine Sicherheitslücke schließen, mit Snapshot vorher und Rückweg bei Rot. Der Proxmox-Host " "startet dabei nicht neu.", "selbst"), Art("rueckweg", "Bei Rot auf den Snapshot zurück", "Nur für ein Gerät, das gerade rot ist. Was seit dem Snapshot dort geschrieben wurde, geht verloren.", "selbst"), Art("suchen", "Nach Updates und Modellen suchen", "Fragt nur nach neuen Versionen, ändert nichts.", "selbst"), Art("updates", "Andere Updates einspielen", "Apps, Pakete, Docker, Motor, llama-swap, Hermes und „Alle aktualisieren“.", "fragen"), Art("aufraeumen", "Alte Kernel entfernen", "Auf dem Proxmox-Host; der laufende und der nächste Kernel bleiben.", "fragen"), Art("radar", "Modelle im Radar testen", "Ein Test belegt die KI-Box einige Minuten; Lucy antwortet dann langsamer.", "fragen"), Art("host_neustart", "Proxmox-Host neu starten", "Alle Container und die Arcane-VM sind ein paar Minuten weg, auch AdGuard (DNS im Heimnetz).", "fragen"), ) _ARTEN = {a.id: a for a in ARTEN} _lock = threading.Lock() def _pfad() -> Path: return einstellungen().daten_dir / "lucy-autonomie.json" def _gespeichert() -> dict[str, str]: try: daten = json.loads(_pfad().read_text(encoding="utf-8")) except (OSError, ValueError): return {} werte = daten.get("stufen") if isinstance(daten, dict) else None return {k: v for k, v in (werte or {}).items() if k in _ARTEN and v in STUFEN} def stufen() -> dict[str, str]: return {a.id: a.standard for a in ARTEN} | _gespeichert() def stufe(art: str) -> str: """Die Stufe einer Aktionsart. Eine unbekannte Art gilt als „fragen“: im Zweifel nie ungefragt.""" return stufen().get(art, "fragen") def stand() -> dict: """Für GET /api/lucy/autonomie (Einstellungen → Lucy) und Lucys Werkzeug freiheiten().""" aktuell = stufen() return {"arten": [{"id": a.id, "name": a.name, "text": a.text, "stufe": aktuell[a.id], "standard": a.standard} for a in ARTEN], "stufen": STUFEN} def setzen(neu: object) -> dict: """POST /api/lucy/autonomie {"stufen": {art: stufe}} — nur bekannte Arten und Stufen. Gespeichert wird, was der Commander gewählt hat, auch wenn es dem Standard entspricht (ändert sich ein Standard, bleibt seine Wahl).""" werte = neu.get("stufen") if isinstance(neu, dict) else None if not isinstance(werte, dict) or not werte: return {"ok": False, "detail": "Es fehlt, was sich ändern soll."} for art, s in werte.items(): if art not in _ARTEN: return {"ok": False, "detail": f"Die Aktionsart „{art}“ gibt es nicht."} if s not in STUFEN: return {"ok": False, "detail": f"„{s}“ ist keine Stufe (lesen, fragen oder selbst)."} with _lock: gespeichert = _gespeichert() | werte pfad = _pfad() pfad.parent.mkdir(parents=True, exist_ok=True) tmp = pfad.with_suffix(".tmp") tmp.write_text(json.dumps({"stufen": gespeichert}, ensure_ascii=False, indent=1), encoding="utf-8") tmp.replace(pfad) teile = [f"{_ARTEN[a].name}: {STUFE_KURZ[s]}" for a, s in werte.items()] log.info("lucy-autonomie: %s", "; ".join(teile)) return {"ok": True, "text": "Gespeichert — " + "; ".join(teile) + "."} def gehandelt(daten: object) -> dict: """POST /api/lucy/gehandelt — die Meldung danach: Lucys Werkzeuge melden hier, was sie auf der Stufe „selbst“ aus eigenem Antrieb getan hat (was, warum, Ergebnis). Nachts kommt das mit der Morgenmeldung.""" from services import rueckfragen d = daten if isinstance(daten, dict) else {} was = " ".join(str(d.get("was") or "").split())[:300].rstrip(".") if not was: return {"ok": False, "detail": "Es fehlt, was Lucy getan hat."} grund = " ".join(str(d.get("grund") or "").split())[:500] ergebnis = " ".join(str(d.get("ergebnis") or "").split())[:500] text = f"{was}." + (f" Warum: {grund}" if grund else "") + (f"\n{ergebnis}" if ergebnis else "") rueckfragen.melden(BETREFF_GEHANDELT, text) log.info("lucy-autonomie: selbst gehandelt — %s", was) return {"ok": True, "text": "Gemeldet."}