ontology: Ontologie als Quelle im Repo, Abgleich mit n8n/KNOW in der Service-Routine
ci / test (push) Successful in 27s
ci / test (push) Successful in 27s
- ontology/know.yaml: 49 Klassen, 66 Beziehungen, 12 Vokabulare (72 Konzepte); erzeugt: know.ttl (OWL + SKOS), Ansicht aus viewer.html.tmpl - knaster.ontology: Modell + Prüfung, Export, Momentaufnahme der n8n-Workflows (nur GET; KNOW-Spezifikation aus dem Workflow-Code, Aufrufe inkl. Laufzeit- Operationen, direktes SQL), Abgleich, Übernahme von Entscheidungen - Service-Routine Schritt 8: Verstöße als Befunde, Ontologie-Änderungen als Vorschläge ontology_change (immer Freigabe); „ordne P-x <konzept> zu“; Entscheidungen in ontology_decisions, `knaster ontology apply` schreibt sie in die YAML (Version +1) - CLI: knaster ontology validate|export|snapshot|check|apply; knaster validate prüft Modell und Aktualität der Turtle-Datei - Gegen echten n8n-Export geprüft: neue Befunde B31–B33 (verified/confirmed, KAN ruft unbekannte Operation, MPA ordnet Projektereignisse nie ein) - SPEC-ontology, BETRIEB §6, Router-Image enthält die Ontologie Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,7 @@ class MaintenanceConfig(BaseModel):
|
||||
adapter_min_cases: int = 300
|
||||
adapter_target_rate: float = 0.90
|
||||
lookback_days: float = 7.0
|
||||
ontology_max_findings: int = 15
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, path: str | Path) -> MaintenanceConfig:
|
||||
@@ -83,7 +84,8 @@ HELP = """Befehle:
|
||||
- status · hilfe · routine · vorschläge
|
||||
- genehmige P-3 [P-5 …] · genehmige alle mit risiko niedrig · lehne P-3 ab: <Grund>
|
||||
- wechsle <slot> auf <basismodell> · trainiere adapter für <cluster> · rollback <slot>
|
||||
- stufe <skill> zurück · markiere <slot> als ok|degraded|unavailable"""
|
||||
- stufe <skill> zurück · markiere <slot> als ok|degraded|unavailable
|
||||
- ordne P-7 <konzept> zu (Vokabel-Vorschlag einem vorhandenen Konzept zuordnen)"""
|
||||
|
||||
|
||||
class LLMIntentMapper:
|
||||
@@ -120,6 +122,7 @@ COMMANDS: list[tuple[str, re.Pattern[str]]] = [
|
||||
("rollback", r"^rollback\s+(?P<slot>[\w.-]+)$"),
|
||||
("demote", r"^(stufe\s+(?P<skill>[\w.-]+)\s+zurück|degradiere\s+(?P<skill2>[\w.-]+))$"),
|
||||
("mark", r"^markiere\s+(?P<slot>[\w.-]+)\s+(als\s+)?(?P<status>ok|degraded|unavailable)$"),
|
||||
("map_concept", rf"^ordne\s+(?P<id>{_ID})\s+(dem\s+konzept\s+)?(?P<concept>[\w.-]+)\s+zu$"),
|
||||
("confirm", r"^(ja|yes|j|ok)$"),
|
||||
("cancel", r"^(nein|no|n|abbrechen)$"),
|
||||
]
|
||||
@@ -143,6 +146,7 @@ class MaintenanceAgent:
|
||||
def __init__(self, *, store: Store, registry: Registry, library: SkillLibrary, index: SkillIndex,
|
||||
policy: Policy, actions: ActionExecutor, lifecycle: Lifecycle,
|
||||
config: MaintenanceConfig | None = None, intent_mapper: IntentMapper | None = None,
|
||||
ontology_check: Callable[[], list[Any]] | None = None,
|
||||
clock: Callable[[], float] = now, wallclock: Callable[[], datetime] = datetime.now):
|
||||
self.store = store
|
||||
self.registry = registry
|
||||
@@ -153,6 +157,7 @@ class MaintenanceAgent:
|
||||
self.lifecycle = lifecycle
|
||||
self.config = config or MaintenanceConfig()
|
||||
self.intent_mapper = intent_mapper
|
||||
self.ontology_check = ontology_check
|
||||
self.clock = clock
|
||||
self.wallclock = wallclock
|
||||
self._pending: dict[str, str] = {} # Sitzung → zu bestätigender Befehl
|
||||
@@ -197,8 +202,9 @@ class MaintenanceAgent:
|
||||
|
||||
def _execute(self, seq: int) -> Proposal:
|
||||
p = self._load(seq)
|
||||
params = {**p.params, "decided_by": p.decided_by} if p.kind == "ontology_change" else p.params
|
||||
try:
|
||||
result = self.actions.execute(p.kind, p.params)
|
||||
result = self.actions.execute(p.kind, params)
|
||||
self.store.update_proposal(seq, state="executed", result=result)
|
||||
except ActionError as exc:
|
||||
self.store.update_proposal(seq, state="failed", result={"ok": False, "message": str(exc)})
|
||||
@@ -212,6 +218,9 @@ class MaintenanceAgent:
|
||||
result={"message": reason or "abgelehnt"})
|
||||
if p.kind == "promote_candidate":
|
||||
self.lifecycle.reject(p.params["candidate"], f"abgelehnt: {reason}")
|
||||
if p.kind == "ontology_change":
|
||||
params = {k: v for k, v in p.params.items() if k != "key"}
|
||||
self.store.set_ontology_decision(p.params["key"], "reject", params, by)
|
||||
return self._load(seq)
|
||||
|
||||
def run_deferred(self) -> list[Proposal]:
|
||||
@@ -318,6 +327,24 @@ class MaintenanceAgent:
|
||||
f"{cand['kind']} für {cand['slot']} übernehmen ({cand['target']})",
|
||||
reason="alle Prüfstufen bestanden", source="lifecycle"))
|
||||
|
||||
# 8. Ontologie ↔ Datenhaltung (SPEC-ontology §3)
|
||||
if self.ontology_check is not None:
|
||||
try:
|
||||
ontology_findings = self.ontology_check()
|
||||
except Exception as exc: # noqa: BLE001 – externe Quelle (n8n-API) darf die Routine nicht abbrechen
|
||||
report.findings.append(f"Ontologie-Abgleich nicht möglich: {type(exc).__name__}: {exc}")
|
||||
ontology_findings = []
|
||||
violations = [f for f in ontology_findings if not f.change]
|
||||
for f in violations[:c.ontology_max_findings]:
|
||||
report.findings.append(f"Ontologie [{f.severity}]: {f.summary}")
|
||||
if len(violations) > c.ontology_max_findings:
|
||||
report.findings.append(f"Ontologie: {len(violations) - c.ontology_max_findings} weitere Befunde "
|
||||
"(knaster ontology check)")
|
||||
for f in ontology_findings:
|
||||
if f.change:
|
||||
add(self.propose("ontology_change", {"key": f.key, **f.change}, f.summary, reason=f.detail,
|
||||
dedup_key=f"ontology:{f.key}"))
|
||||
|
||||
for p in self.run_deferred():
|
||||
report.auto_executed.append(f"{p.id} im Wartungsfenster ausgeführt: {p.summary}")
|
||||
|
||||
@@ -446,3 +473,13 @@ class MaintenanceAgent:
|
||||
return self._new("mark_slot", {"slot": args["slot"], "status": args["status"].lower(),
|
||||
"reason": f"Auftrag von {user}", "set_by": user},
|
||||
f"Slot {args['slot']} als {args['status'].lower()} markieren", user)
|
||||
|
||||
def _cmd_map_concept(self, args: dict[str, str], user: str) -> CommandReply:
|
||||
p = self._load(_seq(args["id"]))
|
||||
if p.kind != "ontology_change" or p.params.get("type") != "vocab_value":
|
||||
raise ValueError(f"{p.id} ist kein Vokabel-Vorschlag")
|
||||
if p.state != "open":
|
||||
raise ValueError(f"{p.id} ist {p.state}")
|
||||
self.store.update_proposal(p.seq, params={**p.params, "decision": "map", "concept": args["concept"]})
|
||||
done = self.approve(p.seq, by=user)
|
||||
return CommandReply(text=self._decided_text(done), proposals=[done])
|
||||
|
||||
Reference in New Issue
Block a user