mitai-jinkendo/backend/data_layer/training_profile/resolver.py
Lars f0e6fd04fb
All checks were successful
Deploy Development / deploy (push) Successful in 50s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 15s
feat: Add personal reference values management in settings and API
- Introduced new routes and API endpoints for managing personal reference values.
- Updated the SettingsPage to include a section for reference values with navigation to manage them.
- Enhanced the backend to support reference values in the data layer and versioning.
- Added necessary imports and UI components for a seamless user experience.
2026-04-06 19:45:06 +02:00

161 lines
5.4 KiB
Python

"""
Layer 1 entry: resolve a multi-dimensional training evaluation from a template.
Pure calculation orchestration — no DB, no HTTP, no formatting for KI/charts.
"""
from __future__ import annotations
from collections import defaultdict
from typing import Any, Dict, List, Mapping, Optional
from data_layer.training_profile.algorithms.registry import get_algorithm
from data_layer.training_profile.models import (
CalculationTemplate,
DimensionResult,
DimensionSpec,
TrainingBaseProfile,
TrainingEvaluationResult,
)
def _required_inputs_present(
activity_inputs: Mapping[str, Any], keys: tuple[str, ...]
) -> tuple[bool, List[str]]:
missing: List[str] = []
for k in keys:
if k not in activity_inputs or activity_inputs[k] is None:
missing.append(k)
return (len(missing) == 0, missing)
def _confidence_level(total_dims: int, dims_with_any_missing: int) -> str:
if total_dims == 0:
return "insufficient"
if dims_with_any_missing == 0:
return "high"
if dims_with_any_missing >= total_dims:
return "insufficient"
if dims_with_any_missing == 1:
return "medium"
return "low"
def _filter_dimensions(
template: CalculationTemplate, base_profile: Optional[TrainingBaseProfile]
) -> tuple[DimensionSpec, ...]:
if base_profile is None or base_profile.allowed_dimension_keys is None:
return template.dimensions
allowed = base_profile.allowed_dimension_keys
return tuple(d for d in template.dimensions if d.key in allowed)
def resolve_training_evaluation(
*,
activity_inputs: Mapping[str, Any],
template: CalculationTemplate,
base_profile: Optional[TrainingBaseProfile] = None,
include_trace: bool = False,
) -> TrainingEvaluationResult:
"""
Run all template dimensions, aggregate Focus Area contributions, attach evidence.
activity_inputs: flat dict (e.g. avg_hr, duration_min, distance_km) supplied by caller.
"""
dimensions = _filter_dimensions(template, base_profile)
dimension_results: List[DimensionResult] = []
contributions: Dict[str, float] = defaultdict(float)
evidence: Dict[str, Any] = {
"dimensions_total": len(dimensions),
"inputs_keys": sorted(activity_inputs.keys()),
}
trace: Optional[Dict[str, Any]] = {} if include_trace else None
dims_with_missing = 0
for spec in dimensions:
ok, missing = _required_inputs_present(activity_inputs, spec.inputs)
if not ok:
dims_with_missing += 1
dimension_results.append(
DimensionResult(
dimension_key=spec.key,
algorithm_id=spec.algorithm_id,
raw_score=0.0,
normalized_score=0.0,
missing_inputs=list(missing),
evidence={"skipped": True, "reason": "required_inputs_missing"},
)
)
if trace is not None:
trace[spec.key] = {"skipped": True, "missing": missing}
continue
algo = get_algorithm(spec.algorithm_id)
slice_inputs = {k: activity_inputs[k] for k in spec.inputs}
run = algo(inputs=slice_inputs, params=dict(spec.params))
if run.missing_inputs:
dims_with_missing += 1
dimension_results.append(
DimensionResult(
dimension_key=spec.key,
algorithm_id=spec.algorithm_id,
raw_score=run.raw_score,
normalized_score=run.normalized_score,
missing_inputs=list(run.missing_inputs),
evidence={"algorithm_detail": run.detail},
)
)
for m in spec.maps_to:
contributions[m.focus_area_key] += run.normalized_score * m.weight
if trace is not None:
trace[spec.key] = {
"inputs": dict(slice_inputs),
"params": dict(spec.params),
"run": {
"raw_score": run.raw_score,
"normalized_score": run.normalized_score,
"missing_inputs": run.missing_inputs,
"detail": run.detail,
},
"maps_to": [(x.focus_area_key, x.weight) for x in spec.maps_to],
}
conf = _confidence_level(len(dimensions), dims_with_missing)
evidence["dimensions_with_missing_or_failed"] = dims_with_missing
return TrainingEvaluationResult(
template_id=template.id,
template_version=template.version,
base_profile_key=base_profile.key if base_profile else None,
dimension_results=dimension_results,
focus_area_contributions=dict(contributions),
confidence=conf,
evidence=evidence,
trace=trace,
)
def resolve_for_base_profile(
*,
activity_inputs: Mapping[str, Any],
base_profile_key: str,
include_trace: bool = False,
) -> TrainingEvaluationResult:
"""Convenience: load profile + default template from registries."""
from data_layer.training_profile.profiles.registry import get_training_base_profile
from data_layer.training_profile.templates.registry import get_calculation_template
profile = get_training_base_profile(base_profile_key)
template = get_calculation_template(profile.default_template_id)
return resolve_training_evaluation(
activity_inputs=activity_inputs,
template=template,
base_profile=profile,
include_trace=include_trace,
)