147 lines
9.7 KiB
Markdown
147 lines
9.7 KiB
Markdown
# SPEC-core: Registry, Skills, Router, Ausführung, Verifier
|
||
|
||
Paket: `core/src/knaster`. Alle Schnittstellen zu Modellen laufen über das OpenAI-kompatible API von LiteLLM.
|
||
|
||
## 1. Registry (`knaster.registry`)
|
||
|
||
Deklarativer Teil (Git): `sites/<cluster>/registry.yaml`. Laufzeitzustand (DB): `slot_state`.
|
||
|
||
```yaml
|
||
slots:
|
||
tier1-general:
|
||
tier: 1 # 0 = stark/extern, 1 = schwaches LLM, (Stufe 2/3 brauchen keinen Slot)
|
||
device: gpu # gpu | cpu | external
|
||
engine: vllm # vllm | llamacpp | openrouter | ...
|
||
node: gpu-main # Rolle/Node, für Healer und Placement
|
||
litellm_model: tier1-general
|
||
base_model: qwen3-14b-awq
|
||
max_context: 32768
|
||
effective_context: 24000 # Startwert; Kalibrierung überschreibt im Laufzeitzustand
|
||
supports_lora: true
|
||
fallbacks: [tier1-flex] # nur bei Infrastruktur-Fehlern, nicht bei Qualitätsfehlern
|
||
adapters:
|
||
- name: rechnungen-v3 # = Modellname, unter dem vLLM den Adapter bereitstellt
|
||
base_model: qwen3-14b-awq
|
||
cluster: rechnungen
|
||
version: 3
|
||
status: active # active | candidate | retired
|
||
```
|
||
|
||
Laufzeitzustand pro Slot: `status ∈ {ok, degraded, unavailable}`, `effective_context` (kalibriert, optional), `reason`, `updated_at`.
|
||
Für den Router gilt: Ein Slot ist **verfügbar**, wenn `status == ok`.
|
||
|
||
## 2. Skills (`knaster.skills`)
|
||
|
||
Dateiformat ist `SKILL.md` mit YAML-Frontmatter, kompatibel zu agentskills/Hermes (`name`, `description`). Eigene Felder stehen unter `metadata.knaster`:
|
||
|
||
| Feld | Typ | Default | Bedeutung |
|
||
|---|---|---|---|
|
||
| `version` | int | 1 | |
|
||
| `status` | draft/active/demoted/retired | draft | nur `active` und `demoted` werden geroutet |
|
||
| `stage` | 0–3 | 0 | freigegebene Ausführungsstufe |
|
||
| `execution` | llm/program/classifier | llm | Art der Ausführung ab Stufe ≥ 1 |
|
||
| `slot` | str | tier1-general | Ziel-Slot für Stufe 1 |
|
||
| `cluster` | str | = name | Adapter-Cluster |
|
||
| `adapter` | auto/none | auto | Adapter verwenden, falls einer aktiv ist |
|
||
| `threshold` | float | 0.80 | minimale Ähnlichkeit |
|
||
| `triggers` | list[str] | [] | zusätzliche Beispielformulierungen fürs Embedding |
|
||
| `output.format` | text/json | text | |
|
||
| `output.schema` | JSON-Schema | – | für den Verifier |
|
||
| `verifier` | list | [] | Prüfungen, siehe §5 |
|
||
| `max_output_tokens` | int | 1024 | |
|
||
| `program` | str | – | Name des registrierten Programms (Stufe 2) |
|
||
| `classifier` | str | – | Name des Classifiers (Stufe 3) |
|
||
|
||
Body: Markdown-Anleitung. Abschnitte, deren Überschrift mit `## Beispiele` beginnt, gelten als **optional**. Die **kompakte Variante** des Skills enthält sie nicht.
|
||
|
||
`demoted` bedeutet: Der Skill wird erkannt, aber auf Stufe 0 ausgeführt, mit dem Skill als Hinweis.
|
||
|
||
## 3. Router (`knaster.router`)
|
||
|
||
Eingabe ist `RouteRequest{input: str, hints?: {skill?: str}}`. Ausgabe ist `RouteDecision`:
|
||
|
||
```
|
||
stage, slot?, model?, skill?, skill_version?, adapter?, prompt_variant (full|compact|none),
|
||
similarity?, estimated_tokens, reasons[], signals[], attempt
|
||
```
|
||
|
||
Ablauf:
|
||
1. Embedding der Eingabe über `EmbeddingClient` (LiteLLM `/v1/embeddings`, Slot `embed-default`).
|
||
2. Kandidaten: Score pro Skill = max(cos(q, description), cos(q, trigger_i)). Davon die Top-k (k = 5).
|
||
3. Optionales Reranking der Top-k (`Reranker`, llama.cpp `/v1/rerank`). Die Reihenfolge folgt dann dem Rerank-Score, der Schwellwert prüft weiterhin die Cosine-Ähnlichkeit.
|
||
4. Gibt es keinen Kandidaten oder liegt die Ähnlichkeit unter `threshold`, wird mit `no_skill` auf Stufe 0 geroutet.
|
||
5. **Konflikt:** Liegt der Zweitplatzierte innerhalb von `conflict_margin` (0,02) und ist kein Reranker aktiv, wird mit `skill_conflict` auf Stufe 0 geroutet. Ist ein Reranker aktiv, wird nur das Signal gesetzt.
|
||
6. Bei `status == demoted` oder `stage == 0` geht es auf Stufe 0, mit Skill als Hinweis.
|
||
7. **Stufe 3/2:** Classifier bzw. Programm, ohne Slot.
|
||
8. **Stufe 1:** Slot des Skills, falls verfügbar, sonst dessen `fallbacks`. Ist keiner verfügbar, wird mit `slot_unavailable` auf Stufe 0 geroutet.
|
||
9. **Token-Budget** (§4): erst `full` versuchen, dann `compact`, dann die Fallback-Slots mit größerem Kontext. Passt nichts, wird mit `context_exceeded` auf Stufe 0 geroutet. Passt es auch auf Stufe 0 nicht, ist das Ergebnis ein Fehler `too_large` mit dem Signal `needs_split`.
|
||
10. **Adapter:** Wenn `slot.supports_lora`, `skill.adapter == auto` und ein aktiver Adapter für `(slot.base_model, skill.cluster)` existiert, wird `model = "<slot.litellm_model>/<adapter.name>"` gesetzt (LiteLLM-Wildcard-Route auf den vLLM des Slots), sonst `model = slot.litellm_model`.
|
||
11. **Canary:** Hat der Slot im Laufzeitzustand ein `canary_model` (vom Lifecycle gesetzt), geht der Anteil `canary_share` der passenden Anfragen auf den Kandidaten. Bei Adapter-Kandidaten gilt das nur für den Cluster des Kandidaten. Die Entscheidung trägt dann `canary: true`.
|
||
|
||
`escalate(decision, reason)`:
|
||
- Bei Infrastruktur-Fehlern (`endpoint_error`, `context_overflow`) wird auf derselben Stufe der nächste Fallback-Slot genommen, sonst wird eine Stufe höher gegangen.
|
||
- Qualitätsfehler (`verifier_failed`, `invalid_format`, `degenerate_output`, `output_truncated`, `classifier_low_confidence`, `program_error`) eskalieren: 3/2 → 1 (falls der Skill `execution: llm` als Rückfall erlaubt, sonst → 0) und 1 → 0.
|
||
- Maximal 3 Eskalationen. Auf Stufe 0 ist danach Schluss, das Ergebnis lautet `failed`.
|
||
|
||
## 4. Token-Budget (`knaster.budget`)
|
||
|
||
`estimate_tokens(text)` ist ein konservativer Schätzer (ceil(len/3)) und ersetzbar durch einen echten Tokenizer pro Slot.
|
||
Bedarf = Systemprompt + Eingabe + `max_output_tokens` + Overhead (64).
|
||
Ein Prompt passt, wenn Bedarf ≤ `effective_context` des Slots (Laufzeitwert vor Registry-Wert).
|
||
|
||
## 5. Ausführung (`knaster.executor`) und Verifier (`knaster.verifier`)
|
||
|
||
Executor:
|
||
- Stufe 1/0: Nachrichten bauen (System = Skill-Body in der gewählten Variante plus Formatanweisung, User = Eingabe), dann `POST /v1/chat/completions`.
|
||
- Stufe 2: registriertes Programm `fn(input:str) -> str`.
|
||
- Stufe 3: `EmbeddingClassifier.predict(vector) -> (label, confidence)`. Liegt die Konfidenz unter dem Minimum, wird `classifier_low_confidence` gemeldet.
|
||
|
||
Erkannte Signale (`knaster.problems.Signal`):
|
||
|
||
| Signal | Erkennung |
|
||
|---|---|
|
||
| `output_truncated` | `finish_reason == "length"` |
|
||
| `degenerate_output` | Anteil wiederholter 4-Gramme > 0,5 bei ≥ 40 Wörtern |
|
||
| `invalid_format` | Format `json`, aber nicht parsebar |
|
||
| `context_overflow` | HTTP 400 mit „context length“ / „maximum context“ / „too many tokens“ im Text |
|
||
| `endpoint_error` | Timeout, Verbindungsfehler, HTTP 5xx/429 |
|
||
|
||
Verifier-Prüfungen (`metadata.knaster.verifier`): `json_schema` (gegen `output.schema`), `regex {pattern}`, `contains {value}`, `not_contains {value}`, `max_chars {value}`, `judge {slot, rubric}` (das LLM antwortet `PASS` oder `FAIL: Grund`).
|
||
Ergebnis ist `VerifyResult{passed, checks[{type, passed, detail}]}`. Bei Format `json` wird `json_schema` immer geprüft, falls ein Schema vorhanden ist.
|
||
|
||
## 6. Fälle und Datensätze (`knaster.cases`)
|
||
|
||
- Tabelle `cases`: id, ts, input, skill, skill_version, stage, slot, model, adapter, messages (JSON, modellneutral), output, passed, signals, split (`train`/`eval`, deterministisch über sha256(input) mod 10 < 2 → eval), origin (`route`/`graduation`/`shadow`).
|
||
- Tabelle `preference_pairs`: Entsteht, wenn in einem `/v1/run` eine Stufe ≥ 1 durch den Verifier fällt und eine höhere Stufe besteht. Inhalt: prompt messages, `rejected`, `chosen`, skill, split.
|
||
- Export als JSONL im Format `{"messages":[...], "meta":{...}}` bzw. `{"prompt":[...],"chosen":"...","rejected":"..."}`.
|
||
|
||
## 7. Kalibrierung (`knaster.calibration`)
|
||
|
||
`effective_context(observations, target=0.9, min_n=20, buckets=[2k,4k,8k,16k,32k,64k,128k])`: Die Beobachtungen (Prompt-Tokens, bestanden) werden in Buckets gruppiert. Ergebnis ist die Obergrenze des größten Buckets, für den gilt: Dieser und alle kleineren Buckets mit n ≥ min_n haben eine Pass-Rate ≥ target. Ohne ausreichende Daten ist das Ergebnis `None`, und der Registry-Wert bleibt.
|
||
|
||
## 8. HTTP-API (`knaster.api`)
|
||
|
||
| Methode | Pfad | Zweck |
|
||
|---|---|---|
|
||
| GET | `/health` | Liveness |
|
||
| GET | `/metrics` | Prometheus |
|
||
| POST | `/v1/route` | RouteDecision |
|
||
| POST | `/v1/run` | komplette Schleife: Route → Ausführung → Verifier → Eskalation, dazu Fall speichern |
|
||
| GET | `/v1/registry` | Slots plus Laufzeitzustand |
|
||
| POST | `/v1/registry/slots/{slot}/state` | Zustand setzen (Healer/Mensch) |
|
||
| POST | `/v1/maintenance/routine` | Service-Routine starten (SPEC-maintenance) |
|
||
| POST | `/v1/maintenance/command` | Prompt-Befehl |
|
||
| GET | `/v1/maintenance/proposals` | offene Vorschläge |
|
||
| POST | `/v1/healer/alertmanager` | Alertmanager-Webhook (SPEC-healer) |
|
||
| GET/POST | `/v1/lifecycle/candidates` | Kandidaten (SPEC-lifecycle) |
|
||
| POST | `/v1/lifecycle/candidates/{id}/report` | Ergebnis eines Jobs melden |
|
||
| POST | `/v1/skills/{name}/graduate` | Graduierungslauf mit mitgelieferten Varianten |
|
||
| POST | `/v1/skills/reload` | Skill-Dateien neu laden (nach Git-Pull) |
|
||
| POST | `/v1/maintenance/proposals/{seq}/approve` bzw. `/reject` | Freigabe per Knopf (n8n/UI) statt per Prompt |
|
||
|
||
## 9. Canary-Prober (`knaster.canary`)
|
||
|
||
Läuft im Service-Prozess (`KNASTER_CANARY_INTERVAL`, Standard 60 s, 0 = aus). Er prüft jeden Slot außer Stufe 0 und Rerank mit einer Mini-Anfrage („Antworte nur mit OK“) bzw. einem Embedding. Daraus entstehen die Metriken `knaster_slot_endpoint_up`, `knaster_slot_canary_ok` und `knaster_slot_canary_latency_seconds`, jeweils mit den Labels `slot, node, docker_host, container, pve_node, vmid` aus `ops` der Registry. Daraus leiten sich die Alerts `SlotEndpointDown` und `SlotCanaryFailing` für den Healer ab.
|
||
|
||
Authentifizierung: Header `Authorization: Bearer <KNASTER_API_TOKEN>` auf allen `/v1/*`-Pfaden.
|