core: Registry, Skills, Router, Executor, Verifier, Runner mit Tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 00:36:21 +02:00
co-authored by Claude Opus 5.5
parent fef930d176
commit f79e2a8556
24 changed files with 2581 additions and 0 deletions
+97
View File
@@ -0,0 +1,97 @@
"""Ausführung einer RouteDecision auf Stufe 0–3 und Erkennung von Laufzeit-Signalen."""
from __future__ import annotations
from typing import Any
from pydantic import BaseModel, Field
from . import programs
from .classifier import EmbeddingClassifier
from .embeddings import Embedder
from .llm import ChatClient, ContextOverflowError, LLMError
from .problems import Signal, extract_json, is_degenerate
from .router import RouteDecision, system_prompt
from .skills import Skill, SkillLibrary
class ExecutionResult(BaseModel):
ok: bool
output: str = ""
signals: list[Signal] = Field(default_factory=list)
messages: list[dict[str, str]] = Field(default_factory=list)
finish_reason: str | None = None
usage: dict[str, Any] = Field(default_factory=dict)
error: str | None = None
label: str | None = None
confidence: float | None = None
def build_messages(skill: Skill | None, decision: RouteDecision, text: str) -> list[dict[str, str]]:
system = system_prompt(skill, decision.prompt_variant)
messages = []
if system:
messages.append({"role": "system", "content": system})
messages.append({"role": "user", "content": text})
return messages
class Executor:
def __init__(self, library: SkillLibrary, chat: ChatClient, embedder: Embedder,
classifiers: dict[str, EmbeddingClassifier] | None = None):
self.library = library
self.chat = chat
self.embedder = embedder
self.classifiers = classifiers or {}
def execute(self, decision: RouteDecision, text: str) -> ExecutionResult:
if decision.status != "routed":
return ExecutionResult(ok=False, error=f"Entscheidung nicht ausführbar: {decision.status}")
skill = self.library.get(decision.skill) if decision.skill else None
if decision.stage == 3:
return self._classify(skill, text)
if decision.stage == 2:
return self._program(skill, text)
return self._llm(skill, decision, text)
def _classify(self, skill: Skill | None, text: str) -> ExecutionResult:
assert skill and skill.meta.classifier
clf = self.classifiers.get(skill.meta.classifier)
if clf is None:
return ExecutionResult(ok=False, signals=[Signal.PROGRAM_ERROR],
error=f"Classifier {skill.meta.classifier} nicht geladen")
label, confidence = clf.predict(self.embedder.embed([text])[0])
if confidence < skill.meta.min_confidence:
return ExecutionResult(ok=False, output=label, label=label, confidence=confidence,
signals=[Signal.CLASSIFIER_LOW_CONFIDENCE])
return ExecutionResult(ok=True, output=label, label=label, confidence=confidence)
def _program(self, skill: Skill | None, text: str) -> ExecutionResult:
assert skill and skill.meta.program
try:
output = programs.get(skill.meta.program)(text)
except Exception as exc: # noqa: BLE001 – Programmfehler sind ein Signal, kein Absturz
return ExecutionResult(ok=False, signals=[Signal.PROGRAM_ERROR], error=f"{type(exc).__name__}: {exc}")
return ExecutionResult(ok=True, output=output)
def _llm(self, skill: Skill | None, decision: RouteDecision, text: str) -> ExecutionResult:
messages = build_messages(skill, decision, text)
try:
res = self.chat.chat(decision.model or "", messages, max_tokens=decision.max_output_tokens)
except ContextOverflowError as exc:
return ExecutionResult(ok=False, messages=messages, signals=[Signal.CONTEXT_OVERFLOW], error=str(exc))
except LLMError as exc:
return ExecutionResult(ok=False, messages=messages, signals=[Signal.ENDPOINT_ERROR], error=str(exc))
signals: list[Signal] = []
if res.finish_reason == "length":
signals.append(Signal.OUTPUT_TRUNCATED)
if is_degenerate(res.content):
signals.append(Signal.DEGENERATE_OUTPUT)
if skill is not None and skill.meta.output.format == "json":
try:
extract_json(res.content)
except ValueError:
signals.append(Signal.INVALID_FORMAT)
return ExecutionResult(ok=not signals, output=res.content, signals=signals, messages=messages,
finish_reason=res.finish_reason, usage=res.usage)