BEHAVIOR-001: learn and predict actuator actions
This commit is contained in:
@@ -13,7 +13,6 @@ from app.actuators.models import (
|
||||
AssignmentSource,
|
||||
LifecycleAuditEntry,
|
||||
LifecycleStatus,
|
||||
ManualOverride,
|
||||
ModelLifecycleState,
|
||||
ReconciliationState,
|
||||
model_id_for_actuator,
|
||||
@@ -57,7 +56,7 @@ _STOPWORDS = frozenset(
|
||||
_NUMERIC_AUTO_ACCEPT_SCORE = 0.82
|
||||
_NUMERIC_MIN_MARGIN = 0.18
|
||||
_CONTEXT_AUTO_ACCEPT_SCORE = 0.78
|
||||
_MAX_CONTEXT_SELECTIONS = 3
|
||||
_MAX_CONTEXT_SELECTIONS = 5
|
||||
_AUDIT_LIMIT = 20
|
||||
|
||||
|
||||
@@ -85,21 +84,6 @@ class ActuatorReconciliationService:
|
||||
def get_actuator(self, actuator_entity_id: str) -> ActuatorRecord:
|
||||
return self._store.get(actuator_entity_id)
|
||||
|
||||
def set_override(
|
||||
self,
|
||||
actuator_entity_id: str,
|
||||
override: ManualOverride | None,
|
||||
) -> ActuatorRecord:
|
||||
record = self._store.get(actuator_entity_id)
|
||||
updated = record.model_copy(
|
||||
update={
|
||||
"manual_override": override,
|
||||
"updated_at": datetime.now(timezone.utc),
|
||||
}
|
||||
)
|
||||
self._store.upsert(updated)
|
||||
return self.reconcile_actuator(actuator_entity_id, trigger="override")
|
||||
|
||||
def delete_actuator(self, actuator_entity_id: str) -> None:
|
||||
model_id = model_id_for_actuator(actuator_entity_id)
|
||||
self._registry.archive(model_id)
|
||||
@@ -132,7 +116,7 @@ class ActuatorReconciliationService:
|
||||
last_summary=(
|
||||
f"{len(refreshed)} Aktuatoren geprüft, "
|
||||
f"{sum(1 for record in refreshed if record.assignment.review_required)} "
|
||||
"mit Prüfbedarf."
|
||||
"mit niedriger Zuordnungssicherheit."
|
||||
),
|
||||
)
|
||||
self._store.save_reconciliation_state(summary)
|
||||
@@ -214,7 +198,6 @@ class ActuatorReconciliationService:
|
||||
actuator=actuator,
|
||||
numeric_candidates=numeric_candidates,
|
||||
context_candidates=context_candidates,
|
||||
override=record.manual_override,
|
||||
)
|
||||
lifecycle = self._reconcile_lifecycle(
|
||||
actuator=actuator,
|
||||
@@ -225,6 +208,7 @@ class ActuatorReconciliationService:
|
||||
updated = record.model_copy(
|
||||
update={
|
||||
"assignment": assignment,
|
||||
"manual_override": None,
|
||||
"numeric_candidates": numeric_candidates,
|
||||
"context_candidates": context_candidates,
|
||||
"lifecycle": lifecycle,
|
||||
@@ -246,27 +230,11 @@ class ActuatorReconciliationService:
|
||||
actuator: HaEntitySummary,
|
||||
numeric_candidates: list[AssignmentCandidate],
|
||||
context_candidates: list[AssignmentCandidate],
|
||||
override: ManualOverride | None,
|
||||
) -> AssignmentSelection:
|
||||
if override is not None:
|
||||
selected_numeric = override.numeric_entity_id
|
||||
selected_contexts = list(dict.fromkeys(override.context_entity_ids))
|
||||
return AssignmentSelection(
|
||||
selected_numeric_entity_id=selected_numeric,
|
||||
selected_context_entity_ids=selected_contexts,
|
||||
source=AssignmentSource.MANUAL,
|
||||
confidence=1.0 if selected_numeric else 0.6,
|
||||
review_required=False,
|
||||
reason=(
|
||||
"Manuelle Zuordnung überschreibt die automatische Heuristik dauerhaft."
|
||||
),
|
||||
)
|
||||
|
||||
top_numeric = numeric_candidates[0] if numeric_candidates else None
|
||||
top_contexts = [
|
||||
candidate.entity_id
|
||||
for candidate in context_candidates
|
||||
if candidate.auto_accepted
|
||||
][: _MAX_CONTEXT_SELECTIONS]
|
||||
if top_numeric is None:
|
||||
return AssignmentSelection(
|
||||
@@ -275,7 +243,10 @@ class ActuatorReconciliationService:
|
||||
source=AssignmentSource.NONE,
|
||||
confidence=0.0,
|
||||
review_required=True,
|
||||
reason=f"Kein numerischer Sensor konnte für {display_name(actuator)} bestimmt werden.",
|
||||
reason=(
|
||||
f"Für {display_name(actuator)} ist noch kein nutzbarer numerischer "
|
||||
"Kontext verfügbar. Die Zuordnung wird automatisch erneut geprüft."
|
||||
),
|
||||
)
|
||||
|
||||
return AssignmentSelection(
|
||||
@@ -285,9 +256,9 @@ class ActuatorReconciliationService:
|
||||
confidence=top_numeric.confidence,
|
||||
review_required=not top_numeric.auto_accepted,
|
||||
reason=(
|
||||
"Automatisch akzeptiert."
|
||||
"Kontext automatisch und eindeutig zugeordnet."
|
||||
if top_numeric.auto_accepted
|
||||
else "Top-Kandidat gefunden, aber Zuordnung ist noch nicht eindeutig genug."
|
||||
else "Besten verfügbaren Kontext automatisch mit niedriger Sicherheit zugeordnet."
|
||||
),
|
||||
)
|
||||
|
||||
@@ -306,14 +277,6 @@ class ActuatorReconciliationService:
|
||||
"Ohne numerische Sensorzuordnung wird kein Modell aktiv gehalten.",
|
||||
now=now,
|
||||
)
|
||||
if assignment.review_required and assignment.source is not AssignmentSource.MANUAL:
|
||||
return self._archive_state(
|
||||
lifecycle,
|
||||
"Zuordnung ist nicht eindeutig; Modell wartet auf Review.",
|
||||
now=now,
|
||||
status=LifecycleStatus.REVIEW_REQUIRED,
|
||||
)
|
||||
|
||||
sensor_id = assignment.selected_numeric_entity_id
|
||||
series = self._read_history(sensor_id, now)
|
||||
points = series.points if series is not None else []
|
||||
@@ -327,7 +290,7 @@ class ActuatorReconciliationService:
|
||||
f"{len(points)} von mindestens {self._settings.min_training_points} "
|
||||
f"Messpunkten für {sensor_id} vorhanden."
|
||||
),
|
||||
"next_action": "Mehr Historie sammeln und Reconciliation erneut ausführen.",
|
||||
"next_action": "Historie wird automatisch weiter gesammelt.",
|
||||
"last_history_point_count": len(points),
|
||||
}
|
||||
),
|
||||
@@ -369,7 +332,7 @@ class ActuatorReconciliationService:
|
||||
"last_history_signature": signature,
|
||||
"last_history_point_count": len(points),
|
||||
"reason": retrain_reason,
|
||||
"next_action": "Automatisch überwachen und bei neuen Daten neu trainieren.",
|
||||
"next_action": "Neue Daten automatisch überwachen und nachtrainieren.",
|
||||
}
|
||||
),
|
||||
action="retrained" if result.replaced else "trained",
|
||||
@@ -384,8 +347,8 @@ class ActuatorReconciliationService:
|
||||
"last_reconciled_at": now,
|
||||
"last_history_signature": signature,
|
||||
"last_history_point_count": len(points),
|
||||
"reason": "Modell ist aktuell und passt zur bestätigten Sensorzuordnung.",
|
||||
"next_action": "Auf neue Historie oder Staleness warten.",
|
||||
"reason": "Modell ist aktuell und passt zur automatischen Kontextzuordnung.",
|
||||
"next_action": "Neue Historie automatisch auswerten.",
|
||||
}
|
||||
),
|
||||
action="kept",
|
||||
@@ -423,7 +386,7 @@ class ActuatorReconciliationService:
|
||||
"status": status,
|
||||
"last_reconciled_at": now,
|
||||
"reason": reason,
|
||||
"next_action": "Review oder neue Zuordnung erforderlich.",
|
||||
"next_action": "Bei neuen Home-Assistant-Daten automatisch erneut zuordnen.",
|
||||
}
|
||||
),
|
||||
action="archived",
|
||||
|
||||
Reference in New Issue
Block a user