From 3cf9af35152ac36a3409dd82a6f5cb4e207357e4 Mon Sep 17 00:00:00 2001 From: Pino Date: Thu, 11 Jun 2026 00:38:45 +0200 Subject: [PATCH] ML-003: Predictor mit Sensor-Validierung und Batch-Interface --- app/ml/predictor.py | 33 ++++++++++++++++++++++++++ tests/ml/test_predictor.py | 48 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 app/ml/predictor.py create mode 100644 tests/ml/test_predictor.py diff --git a/app/ml/predictor.py b/app/ml/predictor.py new file mode 100644 index 0000000..ff02d41 --- /dev/null +++ b/app/ml/predictor.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +import logging +from functools import lru_cache +from typing import Sequence + +from app.ml.feature_store import FeatureVector +from app.ml.training import TrainingPipeline, TrainedArtifact + +logger = logging.getLogger(__name__) + + +class Predictor: + def __init__(self, pipeline: TrainingPipeline) -> None: + self._pipeline = pipeline + + def predict(self, artifact_id: str, entity: FeatureVector) -> str: + artifact = self._pipeline.export(artifact_id) + if entity.sensor_id not in artifact.supported_sensors: + raise ValueError( + f"Sensor '{entity.sensor_id}' wird vom Modell '{artifact_id}' nicht unterstützt." + ) + return f"{artifact_id}:{entity.sensor_id}:{entity.values}" + + def predict_batch(self, artifact_id: str, entities: Sequence[FeatureVector]) -> list[str]: + return [self.predict(artifact_id, entity) for entity in entities] + + @staticmethod + def default_artifact(pipeline: TrainingPipeline) -> TrainedArtifact: + artifacts = list(pipeline._artifacts) + if not artifacts: + raise ValueError("Kein trainiertes Modell gefunden.") + return pipeline.export(artifacts[-1]) \ No newline at end of file diff --git a/tests/ml/test_predictor.py b/tests/ml/test_predictor.py new file mode 100644 index 0000000..3c09a53 --- /dev/null +++ b/tests/ml/test_predictor.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +import pytest + +from app.ml.feature_store import FeatureStore, FeatureVector +from app.ml.predictor import Predictor +from app.ml.training import TrainingPipeline + + +def _vector(sensor_id: str, temperature: float, label: str | None = None) -> FeatureVector: + return FeatureVector(sensor_id=sensor_id, values={"temperature": temperature}, label=label) + + +def predictor() -> Predictor: + store = FeatureStore() + store.add_batch([_vector("sensor.kitchen", 19.0), _vector("sensor.bedroom", 18.5)]) + pipeline = TrainingPipeline(store) + pipeline.run("artifact_v1") + return Predictor(pipeline) + + +def test_predict_returns_expected_format() -> None: + p = predictor() + result = p.predict("artifact_v1", _vector("sensor.kitchen", 21.0)) + assert result == "artifact_v1:sensor.kitchen:{'temperature': 21.0}" + + +def test_predict_rejects_unknown_sensor() -> None: + p = predictor() + with pytest.raises(ValueError): + p.predict("artifact_v1", _vector("sensor.unknown", 10.0)) + + +def test_predict_batch_matches_single_calls() -> None: + p = predictor() + entities = [_vector("sensor.kitchen", 21.0), _vector("sensor.bedroom", 19.0)] + assert p.predict_batch("artifact_v1", entities) == [ + p.predict("artifact_v1", item) for item in entities + ] + + +def test_default_artifact_returns_last_registered() -> None: + store = FeatureStore() + store.add_batch([_vector("sensor.kitchen", 19.0), _vector("sensor.bedroom", 18.5)]) + pipeline = TrainingPipeline(store) + pipeline.run("first") + pipeline.run("second") + assert Predictor.default_artifact(pipeline).artifact_id == "second" \ No newline at end of file