Files
cluster-knaster/core/src/knaster/system.py
T
rorappandClaude Opus 5.5 9442cc49d1
ci / test (push) Successful in 27s
ontology: Ontologie als Quelle im Repo, Abgleich mit n8n/KNOW in der Service-Routine
- 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>
2026-10-13 02:54:49 +02:00

160 lines
7.3 KiB
Python

"""Verdrahtung aller Komponenten (einmal pro Prozess, in Tests mit Fakes)."""
from __future__ import annotations
import json
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from .canary import CanaryProber
from .classifier import EmbeddingClassifier
from .embeddings import Embedder, LlamaCppReranker, OpenAIEmbedder, Reranker
from .executor import Executor
from .healer.drivers import CompositeInfra, InfraDriver, Notifier, PortainerDriver, ProxmoxDriver, WebhookNotifier
from .healer.engine import Healer, HealerConfig
from .lifecycle import JobRunner, Lifecycle, LifecycleConfig, LoggingJobRunner, WebhookJobRunner
from .llm import ChatClient, OpenAIChatClient
from .maintenance.actions import ActionExecutor
from .maintenance.agent import IntentMapper, LLMIntentMapper, MaintenanceAgent, MaintenanceConfig
from .metrics import Metrics
from .ontology import Ontology, OntologyFinding, build_snapshot, check, fetch_workflows
from .policy import Policy
from .registry import Registry
from .router import Router
from .runner import Runner
from .settings import Settings
from .skills import SkillLibrary
from .store import Store
from .util import now
from .verifier import Verifier
@dataclass
class System:
settings: Settings
store: Store
registry: Registry
library: SkillLibrary
embedder: Embedder
chat: ChatClient
router: Router
executor: Executor
verifier: Verifier
runner: Runner
policy: Policy
lifecycle: Lifecycle
actions: ActionExecutor
maintenance: MaintenanceAgent
healer: Healer
metrics: Metrics
prober: CanaryProber
def _load_classifiers(directory: Path) -> dict[str, EmbeddingClassifier]:
if not directory.is_dir():
return {}
return {p.stem: EmbeddingClassifier.load(p) for p in sorted(directory.glob("*.json"))}
def _optional(path: Path, loader: Callable, default): # type: ignore[no-untyped-def]
return loader(path) if path.exists() else default
def ontology_source_for(settings: Settings) -> Callable[[], list[dict]] | None:
"""Woher die n8n-Workflows für den Ontologie-Abgleich kommen: Datei (offline) oder n8n-API (nur lesend)."""
if settings.ontology_snapshot:
path = Path(settings.ontology_snapshot)
def from_file() -> list[dict]:
data = json.loads(path.read_text(encoding="utf-8"))
return data.get("data", data) if isinstance(data, dict) else data
return from_file
if settings.n8n_host and settings.n8n_api_key:
return lambda: fetch_workflows(settings.n8n_host, settings.n8n_api_key, timeout=settings.request_timeout)
return None
def ontology_checker(settings: Settings, store: Store,
source: Callable[[], list[dict]] | None) -> Callable[[], list[OntologyFinding]] | None:
source = source or ontology_source_for(settings)
if source is None or not settings.ontology_path.exists():
return None
def run() -> list[OntologyFinding]:
onto = Ontology.from_file(settings.ontology_path) # bei jedem Lauf neu: Änderungen im Repo zählen sofort
decided = {d["key"] for d in store.list_ontology_decisions()}
return check(onto, build_snapshot(source()), decided=decided)
return run
def build_system(
settings: Settings,
*,
embedder: Embedder | None = None,
chat: ChatClient | None = None,
reranker: Reranker | None = None,
infra: InfraDriver | None = None,
notifier: Notifier | None = None,
jobs: JobRunner | None = None,
intent_mapper: IntentMapper | None = None,
ontology_source: Callable[[], list[dict]] | None = None,
clock: Callable[[], float] = now,
wallclock: Callable[[], datetime] = datetime.now,
) -> System:
if settings.db_url.startswith("sqlite:///") and not settings.db_url.startswith("sqlite:///:"):
Path(settings.db_url.removeprefix("sqlite:///")).parent.mkdir(parents=True, exist_ok=True)
store = Store(settings.db_url)
registry = Registry.from_file(settings.site_dir / "registry.yaml", store)
library = SkillLibrary.from_dir(settings.skills_dir, store)
chat = chat or OpenAIChatClient(settings.litellm_base_url, settings.litellm_api_key, settings.request_timeout)
embedder = embedder or OpenAIEmbedder(settings.litellm_base_url, settings.embed_model, settings.litellm_api_key)
if reranker is None and settings.rerank_url:
reranker = LlamaCppReranker(settings.rerank_url, settings.rerank_model)
cfg = settings.config_dir
policy = _optional(cfg / "policy.yaml", Policy.from_file, Policy())
healer_cfg = _optional(cfg / "healer-rules.yaml", HealerConfig.from_file, HealerConfig())
maint_cfg = _optional(cfg / "maintenance.yaml", MaintenanceConfig.from_file, MaintenanceConfig())
life_cfg = _optional(cfg / "lifecycle.yaml", LifecycleConfig.from_file, LifecycleConfig())
metrics = Metrics()
router = Router(registry, library, embedder, reranker)
executor = Executor(library, chat, embedder, _load_classifiers(cfg / "classifiers"))
verifier = Verifier(registry, chat)
runner = Runner(router, executor, verifier, library, store, metrics)
if jobs is None:
use_webhook = settings.job_webhook_url and not settings.dry_run
jobs = WebhookJobRunner(settings.job_webhook_url, settings.webhook_auth) if use_webhook else LoggingJobRunner()
lifecycle = Lifecycle(store, registry, jobs, life_cfg)
if infra is None and not settings.dry_run:
portainer = PortainerDriver(settings.portainer_host, settings.portainer_key) \
if settings.portainer_host and settings.portainer_key else None
proxmox = ProxmoxDriver(settings.proxmox_url, settings.proxmox_token_id, settings.proxmox_token_secret) \
if settings.proxmox_url and settings.proxmox_token_id else None
infra = CompositeInfra(portainer, proxmox)
if notifier is None and settings.notify_webhook_url:
notifier = WebhookNotifier(settings.notify_webhook_url, settings.webhook_auth)
actions = ActionExecutor(store=store, registry=registry, library=library, lifecycle=lifecycle, infra=infra,
notifier=notifier, dry_run=settings.dry_run,
enabled_infra_actions=set(healer_cfg.enabled_actions), on_skills_changed=router.refresh)
if intent_mapper is None and registry.has_slot("tier1-general"):
intent_mapper = LLMIntentMapper(chat, registry.slot("tier1-general").spec.litellm_model)
maintenance = MaintenanceAgent(store=store, registry=registry, library=library, index=router.index,
policy=policy, actions=actions, lifecycle=lifecycle, config=maint_cfg,
intent_mapper=intent_mapper,
ontology_check=ontology_checker(settings, store, ontology_source),
clock=clock, wallclock=wallclock)
healer = Healer(healer_cfg, store, actions, policy, propose=maintenance.propose, clock=clock)
prober = CanaryProber(registry, chat, embedder_for={settings.embed_model: embedder},
metrics_registry=metrics.registry)
return System(settings, store, registry, library, embedder, chat, router, executor, verifier, runner, policy,
lifecycle, actions, maintenance, healer, metrics, prober)