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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
dd23f3fb4c
commit
12cefb427a
@@ -0,0 +1,133 @@
|
||||
"""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."}
|
||||
Reference in New Issue
Block a user