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>
160 lines
7.3 KiB
Python
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)
|