Files
cluster-knaster/core/src/knaster/executor.py
T

98 lines
4.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)