Files
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

223 lines
6.7 KiB
Python

from __future__ import annotations
import textwrap
from collections.abc import Callable
from pathlib import Path
import pytest
from knaster.embeddings import HashingEmbedder
from knaster.llm import ChatResult
from knaster.registry import Registry
from knaster.skills import SkillLibrary
from knaster.store import Store
REGISTRY = {
"slots": {
"tier0-strong": {
"tier": 0, "device": "external", "engine": "openrouter", "litellm_model": "tier0-strong",
"base_model": "strong-x", "max_context": 128000, "effective_context": 100000,
},
"tier1-general": {
"tier": 1, "engine": "vllm", "node": "gpu-main", "litellm_model": "tier1-general",
"base_model": "base-14b", "max_context": 8192, "effective_context": 2000,
"supports_lora": True, "fallbacks": ["tier1-flex"],
},
"tier1-flex": {
"tier": 1, "engine": "vllm", "node": "gpu-flex", "litellm_model": "tier1-flex",
"base_model": "base-14b", "max_context": 32768, "effective_context": 16000,
"supports_lora": True,
},
"verifier-small": {
"tier": 1, "engine": "llamacpp", "node": "gpu-small", "litellm_model": "verifier-small",
"base_model": "small-4b", "max_context": 8192,
},
"embed-default": {
"tier": 1, "engine": "llamacpp", "node": "gpu-embed", "litellm_model": "embed-default",
"base_model": "embed-0.6b", "max_context": 8192, "kind": "embedding",
},
},
"adapters": [
{"name": "rechnungen-v1", "base_model": "base-14b", "cluster": "rechnungen", "version": 1},
],
}
SKILLS = {
"rechnung-extrahieren": """\
---
name: rechnung-extrahieren
description: Rechnung auslesen und Rechnungsnummer Betrag Datum als JSON extrahieren
metadata:
knaster:
status: active
stage: 1
cluster: rechnungen
threshold: 0.3
triggers: ["Rechnungsdaten aus einer Rechnung extrahieren"]
max_output_tokens: 300
output:
format: json
schema:
type: object
required: [nummer, betrag]
properties:
nummer: {type: string}
betrag: {type: number}
---
# Rechnung extrahieren
Lies Nummer und Betrag aus.
## Beispiele
Eingabe: Rechnung 7 über 10 EUR -> {"nummer": "7", "betrag": 10}
""",
"ticket-kategorisieren": """\
---
name: ticket-kategorisieren
description: Support Ticket einer Kategorie zuordnen Drucker Netzwerk Passwort
metadata:
knaster:
status: active
stage: 3
execution: classifier
classifier: ticket-kat
threshold: 0.3
min_confidence: 0.6
---
Ordne das Ticket einer Kategorie zu: drucker, netzwerk, passwort.
""",
"text-gross": """\
---
name: text-gross
description: Text in Großbuchstaben umwandeln uppercase
metadata:
knaster:
status: active
stage: 2
execution: program
program: echo-upper
threshold: 0.3
---
Wandle den Text in Großbuchstaben um.
""",
"entwurf": """\
---
name: entwurf
description: Gedicht über Rechnung schreiben
metadata:
knaster:
status: draft
---
Entwurf.
""",
"vertrag-pruefen": """\
---
name: vertrag-pruefen
description: Vertrag prüfen Kündigungsfrist Laufzeit Klauseln
metadata:
knaster:
status: demoted
stage: 1
threshold: 0.3
verifier:
- {type: contains, value: "Kündigungsfrist"}
---
Prüfe den Vertrag.
""",
}
@pytest.fixture
def store() -> Store:
return Store("sqlite://")
@pytest.fixture
def registry(store: Store) -> Registry:
return Registry.from_dict(REGISTRY, store)
@pytest.fixture
def skills_dir(tmp_path: Path) -> Path:
for name, text in SKILLS.items():
d = tmp_path / "skills" / name
d.mkdir(parents=True)
(d / "SKILL.md").write_text(textwrap.dedent(text), encoding="utf-8")
return tmp_path / "skills"
@pytest.fixture
def library(skills_dir: Path, store: Store) -> SkillLibrary:
return SkillLibrary.from_dir(skills_dir, store)
@pytest.fixture
def embedder() -> HashingEmbedder:
return HashingEmbedder()
class FakeChat:
"""Antworten pro Modellname; ein Handler darf ChatResult liefern oder eine Exception werfen."""
def __init__(self, handlers: dict[str, Callable[[list[dict[str, str]]], ChatResult | str]] | None = None):
self.handlers = handlers or {}
self.calls: list[tuple[str, list[dict[str, str]]]] = []
def chat(self, model, messages, *, max_tokens=None, temperature=None):
self.calls.append((model, messages))
handler = self.handlers.get(model)
if handler is None:
return ChatResult(content="ok", finish_reason="stop")
result = handler(messages)
if isinstance(result, str):
return ChatResult(content=result, finish_reason="stop")
return result
@pytest.fixture
def fake_chat() -> FakeChat:
return FakeChat()
class Clock:
def __init__(self, t: float | None = None):
import time
self.t = time.time() if t is None else t
def __call__(self) -> float:
return self.t
def advance(self, seconds: float) -> None:
self.t += seconds
@pytest.fixture
def clock() -> Clock:
return Clock()
@pytest.fixture
def make_system(tmp_path, skills_dir, fake_chat, clock):
import yaml
from knaster.embeddings import HashingEmbedder
from knaster.healer.drivers import DryRunDriver
from knaster.lifecycle import LoggingJobRunner
from knaster.settings import REPO_ROOT, Settings
from knaster.system import build_system
def make(**overrides):
site = tmp_path / "site"
site.mkdir(exist_ok=True)
(site / "registry.yaml").write_text(yaml.safe_dump(REGISTRY))
settings = Settings(db_url="sqlite://", site_dir=site, skills_dir=skills_dir,
config_dir=overrides.pop("config_dir", REPO_ROOT / "config"),
api_token=overrides.pop("api_token", "secret"),
ontology_path=overrides.pop("ontology_path", tmp_path / "keine-ontologie.yaml"))
kw = dict(embedder=HashingEmbedder(), chat=fake_chat, jobs=LoggingJobRunner(), notifier=DryRunDriver(),
clock=clock)
kw.update(overrides)
return build_system(settings, **kw)
return make