core: Registry, Skills, Router, Executor, Verifier, Runner mit Tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user