Add safety dashboard and decision transparency
Some checks failed
quality / test (3.11) (push) Has been cancelled
quality / test (3.13) (push) Has been cancelled

This commit is contained in:
2026-06-17 18:26:49 +02:00
parent ca253d1e6c
commit 0101596e93
12 changed files with 792 additions and 23 deletions

View File

@@ -37,6 +37,21 @@ class BehaviorStatus(StrEnum):
BLOCKED = "blocked"
class SafetyStage(StrEnum):
OBSERVE = "observe"
SUGGEST = "suggest"
SHADOW = "shadow"
PARTIAL = "partial"
ACTIVE = "active"
class JobStatus(StrEnum):
PENDING = "pending"
RUNNING = "running"
COMPLETED = "completed"
FAILED = "failed"
class AssignmentCandidate(BaseModel):
entity_id: str
domain: str
@@ -121,6 +136,61 @@ class BehaviorPrediction(BaseModel):
execution_reason: str = "Vorhersage wurde noch nicht ausgeführt."
class DecisionFactor(BaseModel):
entity_id: str | None = None
label: str
factor_type: str = Field(max_length=40)
state: str | None = None
weight: float = Field(default=1.0, ge=0.0, le=1.0)
contribution: float = Field(default=0.0, ge=0.0, le=1.0)
evidence: list[str] = Field(default_factory=list)
class SafetyRule(BaseModel):
rule_id: str = Field(pattern=r"^[a-z0-9_-]{1,64}$")
label: str = Field(min_length=1, max_length=160)
enabled: bool = True
blocking: bool = True
reason: str = Field(default="", max_length=300)
def default_safety_rules() -> list[SafetyRule]:
return [
SafetyRule(
rule_id="activation_ready",
label="Nur nach Lernfreigabe aktiv schalten",
reason="Der Aktor muss genug eindeutiges Verhalten gelernt haben.",
),
SafetyRule(
rule_id="confidence_threshold",
label="Mindest-Sicherheit einhalten",
reason="Vorhersagen unter der Schaltschwelle bleiben im Shadow-Modus.",
),
SafetyRule(
rule_id="cooldown",
label="Sicherheits-Cooldown gegen Hin-und-her-Schalten",
reason="Gleiche Zielzustände werden nicht zu schnell wiederholt.",
),
SafetyRule(
rule_id="manual_block",
label="Manuelle Sperre respektieren",
reason="Nutzer können jeden Aktor sofort blockieren.",
),
]
class SafetyProfile(BaseModel):
stage: SafetyStage = SafetyStage.SHADOW
manual_block: bool = False
min_confidence: float = Field(default=0.82, ge=0.0, le=1.0)
min_confidence_on: float | None = Field(default=None, ge=0.0, le=1.0)
min_confidence_off: float | None = Field(default=None, ge=0.0, le=1.0)
cooldown_seconds: int | None = Field(default=None, ge=0)
rules: list[SafetyRule] = Field(default_factory=default_safety_rules)
updated_at: datetime | None = None
note: str | None = Field(default=None, max_length=500)
class ExecutionEvent(BaseModel):
target_state: str
executed_at: datetime
@@ -150,6 +220,16 @@ class BehaviorState(BaseModel):
related_automations: list[RelatedAutomation] = Field(default_factory=list)
paused_automation_entity_ids: list[str] = Field(default_factory=list)
reason: str = "Historische Aktorhandlungen werden analysiert."
safety: SafetyProfile = Field(default_factory=SafetyProfile)
decision_factors: list[DecisionFactor] = Field(default_factory=list)
knowledge: list[str] = Field(default_factory=list)
assumptions: list[str] = Field(default_factory=list)
uncertainties: list[str] = Field(default_factory=list)
safety_blockers: list[str] = Field(default_factory=list)
sample_trend: list[int] = Field(default_factory=list)
confidence_trend: list[float] = Field(default_factory=list)
correct_feedback_count: int = Field(default=0, ge=0)
incorrect_feedback_count: int = Field(default=0, ge=0)
class ActuatorRecord(BaseModel):
@@ -176,5 +256,22 @@ class ReconciliationState(BaseModel):
last_summary: str = "Noch keine Reconciliation ausgeführt."
class JobQueueItem(BaseModel):
job_id: str = Field(min_length=1, max_length=120)
kind: str = Field(min_length=1, max_length=40)
target: str | None = Field(default=None, max_length=160)
trigger: str = Field(default="manual", max_length=40)
status: JobStatus = JobStatus.PENDING
started_at: datetime | None = None
completed_at: datetime | None = None
duration_ms: int | None = Field(default=None, ge=0)
error: str | None = Field(default=None, max_length=500)
summary: str = Field(default="", max_length=500)
class JobQueueState(BaseModel):
jobs: list[JobQueueItem] = Field(default_factory=list)
def model_id_for_actuator(actuator_entity_id: str) -> str:
return f"actuator.{actuator_entity_id}"