""" Training Type Profiles - Master Evaluator Comprehensive activity evaluation across all 7 dimensions. Issue: #15 Date: 2026-03-23 """ from typing import Dict, Optional, List from datetime import datetime import logging from rule_engine import RuleEvaluator, IntensityZoneEvaluator, TrainingEffectsEvaluator logger = logging.getLogger(__name__) class TrainingProfileEvaluator: """ Master class for comprehensive activity evaluation. Evaluates an activity against a training type profile across 7 dimensions: 1. Minimum Requirements (Quality Gates) 2. Intensity Zones (HR zones) 3. Training Effects (Abilities) 4. Periodization (Frequency & Recovery) 5. Performance Indicators (KPIs) 6. Safety (Warnings) 7. AI Context """ def __init__(self, parameters_registry: Dict[str, Dict]): """ Initialize evaluator with parameter registry. Args: parameters_registry: Dict mapping parameter_key -> config """ self.parameters_registry = parameters_registry self.rule_evaluator = RuleEvaluator() self.zone_evaluator = IntensityZoneEvaluator() self.effects_evaluator = TrainingEffectsEvaluator() def evaluate_activity( self, activity: Dict, training_type_profile: Optional[Dict], context: Optional[Dict] = None ) -> Dict: """ Complete evaluation of an activity against its training type profile. Args: activity: Activity data dictionary training_type_profile: Training type profile (JSONB) context: { "user_profile": {...}, "recent_activities": [...], "historical_activities": [...] } Returns: { "evaluated_at": ISO timestamp, "profile_version": str, "rule_set_results": { "minimum_requirements": {...}, "intensity_zones": {...}, "training_effects": {...}, "periodization": {...}, "performance_indicators": {...}, "safety": {...} }, "overall_score": float (0-1), "quality_label": str, "recommendations": [str], "warnings": [str] } """ # No profile? Return unvalidated result if not training_type_profile: return self._create_unvalidated_result() rule_sets = training_type_profile.get("rule_sets", {}) context = context or {} results = { "evaluated_at": datetime.now().isoformat(), "profile_version": training_type_profile.get("version", "unknown"), "rule_set_results": {} } # ━━━ 1. MINIMUM REQUIREMENTS ━━━ if "minimum_requirements" in rule_sets: results["rule_set_results"]["minimum_requirements"] = \ self.rule_evaluator.evaluate_rule_set( rule_sets["minimum_requirements"], activity, self.parameters_registry ) # ━━━ 2. INTENSITY ZONES ━━━ if "intensity_zones" in rule_sets: results["rule_set_results"]["intensity_zones"] = \ self.zone_evaluator.evaluate( rule_sets["intensity_zones"], activity, context.get("user_profile", {}) ) # ━━━ 3. TRAINING EFFECTS ━━━ if "training_effects" in rule_sets: results["rule_set_results"]["training_effects"] = \ self.effects_evaluator.evaluate( rule_sets["training_effects"], activity, results["rule_set_results"].get("intensity_zones") ) # ━━━ 4. PERIODIZATION ━━━ if "periodization" in rule_sets: results["rule_set_results"]["periodization"] = \ self._evaluate_periodization( rule_sets["periodization"], activity, context.get("recent_activities", []) ) # ━━━ 5. PERFORMANCE INDICATORS ━━━ if "performance_indicators" in rule_sets: results["rule_set_results"]["performance_indicators"] = \ self._evaluate_performance( rule_sets["performance_indicators"], activity, context.get("historical_activities", []) ) # ━━━ 6. SAFETY WARNINGS ━━━ if "safety" in rule_sets: results["rule_set_results"]["safety"] = \ self._evaluate_safety( rule_sets["safety"], activity ) # ━━━ OVERALL SCORE & QUALITY LABEL ━━━ overall_score = self._calculate_overall_score(results["rule_set_results"]) results["overall_score"] = overall_score results["quality_label"] = self._get_quality_label(overall_score) # ━━━ RECOMMENDATIONS & WARNINGS ━━━ results["recommendations"] = self._generate_recommendations(results) results["warnings"] = self._collect_warnings(results) return results def _create_unvalidated_result(self) -> Dict: """Creates result for activities without profile.""" return { "evaluated_at": datetime.now().isoformat(), "profile_version": None, "rule_set_results": {}, "overall_score": None, "quality_label": None, "recommendations": ["Kein Trainingsprofil konfiguriert"], "warnings": [] } def _evaluate_periodization( self, config: Dict, activity: Dict, recent_activities: List[Dict] ) -> Dict: """ Evaluates periodization compliance (frequency & recovery). Simplified for MVP - full implementation later. """ if not config.get("enabled", False): return {"enabled": False} # Basic frequency check training_type_id = activity.get("training_type_id") same_type_this_week = sum( 1 for a in recent_activities if a.get("training_type_id") == training_type_id ) frequency_config = config.get("frequency", {}) optimal = frequency_config.get("per_week_optimal", 3) return { "enabled": True, "weekly_count": same_type_this_week, "optimal_count": optimal, "frequency_status": "optimal" if same_type_this_week <= optimal else "over_optimal", "recovery_adequate": True, # Simplified for MVP "warning": None } def _evaluate_performance( self, config: Dict, activity: Dict, historical_activities: List[Dict] ) -> Dict: """ Evaluates performance development. Simplified for MVP - full implementation later. """ if not config.get("enabled", False): return {"enabled": False} return { "enabled": True, "trend": "stable", # Simplified "metrics_comparison": {}, "benchmark_level": "intermediate" } def _evaluate_safety(self, config: Dict, activity: Dict) -> Dict: """ Evaluates safety warnings. """ if not config.get("enabled", False): return {"enabled": False, "warnings": []} warnings_config = config.get("warnings", []) triggered_warnings = [] for warning_rule in warnings_config: param_key = warning_rule.get("parameter") operator = warning_rule.get("operator") threshold = warning_rule.get("value") severity = warning_rule.get("severity", "medium") message = warning_rule.get("message", "") actual_value = activity.get(param_key) if actual_value is not None: operator_func = RuleEvaluator.OPERATORS.get(operator) if operator_func and operator_func(actual_value, threshold): triggered_warnings.append({ "severity": severity, "message": message, "parameter": param_key, "actual_value": actual_value, "threshold": threshold }) return { "enabled": True, "warnings": triggered_warnings } def _calculate_overall_score(self, rule_set_results: Dict) -> float: """ Calculates weighted overall score. Weights: - Minimum Requirements: 40% - Intensity Zones: 20% - Periodization: 20% - Performance: 10% - Training Effects: 10% """ weights = { "minimum_requirements": 0.4, "intensity_zones": 0.2, "periodization": 0.2, "performance_indicators": 0.1, "training_effects": 0.1 } total_score = 0.0 total_weight = 0.0 for rule_set_name, weight in weights.items(): result = rule_set_results.get(rule_set_name) if result and result.get("enabled"): score = result.get("score", 0.5) # Special handling for different result types if rule_set_name == "intensity_zones": score = result.get("duration_quality", 0.5) elif rule_set_name == "periodization": score = 1.0 if result.get("recovery_adequate", False) else 0.5 total_score += score * weight total_weight += weight return round(total_score / total_weight, 2) if total_weight > 0 else 0.5 def _get_quality_label(self, score: Optional[float]) -> Optional[str]: """Converts score to quality label.""" if score is None: return None if score >= 0.9: return "excellent" elif score >= 0.7: return "good" elif score >= 0.5: return "acceptable" else: return "poor" def _generate_recommendations(self, results: Dict) -> List[str]: """Generates actionable recommendations.""" recommendations = [] # Check minimum requirements min_req = results["rule_set_results"].get("minimum_requirements", {}) if min_req.get("enabled") and not min_req.get("passed"): for failed in min_req.get("failed_rules", []): param = failed.get("parameter") actual = failed.get("actual_value") expected = failed.get("expected_value") reason = failed.get("reason", "") symbol = failed.get("operator_symbol", "") recommendations.append( f"{param}: {actual} {symbol} {expected} - {reason}" ) # Check intensity zones zone_result = results["rule_set_results"].get("intensity_zones", {}) if zone_result.get("enabled") and zone_result.get("recommendation"): recommendations.append(zone_result["recommendation"]) # Default recommendation if excellent if results.get("quality_label") == "excellent" and not recommendations: recommendations.append("Hervorragendes Training! Weiter so.") return recommendations def _collect_warnings(self, results: Dict) -> List[str]: """Collects all warnings from safety checks.""" safety_result = results["rule_set_results"].get("safety", {}) if not safety_result.get("enabled"): return [] warnings = [] for warning in safety_result.get("warnings", []): severity_icon = "🔴" if warning["severity"] == "high" else "⚠️" warnings.append(f"{severity_icon} {warning['message']}") return warnings