Files
mission-control-v2/backend/services/lucy_autonomie.py
T
HitonabiandClaude Opus 5.5 12cefb427a Welle 3 · Lucys Freiheiten je Aktionsart, Rückfragen mit „ja 7“, Lucy untersucht rote Hinweise, Wochenbericht
- services/lucy_autonomie.py: Stufe je Aktionsart (nur lesen, erst fragen, selbst handeln), Start „mehr Freiheit“;
  Meldung danach (POST /api/lucy/gehandelt)
- services/rueckfragen.py: nummerierte Rückfragen, nur Knöpfe der Oberfläche (ERLAUBT), „ja“ führt genau die
  gespeicherte Aktion aus, 24 h gültig, hinfällig wenn der Hinweis weg ist, Nachprüfung danach
- notify.sh -k: Antworttasten (Antworttastatur über den Direktweg, ohne Eingriff in Hermes)
- services/ermittlung.py: Wächter meldet rote Hinweise an Lucy; Knopf wird Rückfrage, Lucy sieht über den
  api_server nach und schickt Ursache und Weg als eine Nachricht
- services/wochenbericht.py + mc2-wochenbericht.timer (So 07:30): feste Sätze, Absatz vom Modell, offene
  App-Updates als Rückfragen
- MCP: Werkzeuge fragen die Freiheiten (auf_bitte), neu: freiheiten, rueckfragen, rueckfrage_beantworten,
  sicherheitsupdates_einspielen, snapshot_zurueckspielen, snapshots, geraet_logs, dienst_logs
- Ausführer-Aktion journal (nur lesend), POST /api/homelab/ziele/{id}/sicherheitsupdates
- Läufe des Homelabs mit flock (Oberfläche und Steward schreiben beide)
- Skill homelab-orchestrator, SOUL-Abschnitt als Kopie in deploy/nur-box

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 19:12:50 +02:00

134 lines
6.3 KiB
Python

"""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."}