runner-ai-intelligence / src /agents /guardrail_agent.py
avfranco's picture
HF Space deploy snapshot (minimal allow-list)
d64fd55
from typing import Dict, Any, List, Optional
import logging
import time
from observability import logger as obs_logger
from observability import components as obs_components
from .base import BaseAgent
from domain.training.agent_models import RiskAssessment, WeeklySummary
from domain.runner.profile import RunnerProfile
logger = logging.getLogger(__name__)
class InjuryFatigueGuardrailAgent(BaseAgent):
"""
Heuristic-based agent that assesses the risk of injury or fatigue.
"""
def assess_risk(
self,
features: WeeklySummary,
pain_reported: bool = False,
profile: Optional[RunnerProfile] = None,
) -> RiskAssessment:
"""
Calculates risk level and reasons based on running features.
"""
risk_level = "LOW"
reasons = []
adjustments = []
metrics = {}
# 1. Mileage Spike Heuristic
# summary often contains 'weekly_km'
weekly_km = features.weekly_km
if weekly_km:
weeks = sorted(weekly_km.items(), reverse=True)
if len(weeks) >= 2:
current_week_val = weeks[0][1]
prev_week_val = weeks[1][1]
metrics["current_week_km"] = current_week_val
metrics["prev_week_km"] = prev_week_val
if prev_week_val > 0:
increase_pct = (current_week_val - prev_week_val) / prev_week_val
metrics["wow_increase_pct"] = increase_pct
if increase_pct > 0.5:
risk_level = "HIGH"
reasons.append(f"Major mileage spike: {increase_pct:.1%} increase Wow.")
adjustments.append("Reduce volume significantly; include extra rest day.")
elif increase_pct > 0.3:
if risk_level != "HIGH":
risk_level = "MEDIUM"
reasons.append(
f"Significant mileage increase: {increase_pct:.1%} increase WoW."
)
adjustments.append("Cap intensity; avoid two hard days in a row.")
# 1.5. Baseline Mileage Spike Heuristic
if (
profile
and isinstance(profile.baseline_weekly_km, (int, float))
and profile.baseline_weekly_km > 0
):
current_week_val = 0.0
if weekly_km:
weeks = sorted(weekly_km.items(), reverse=True)
if weeks:
current_week_val = weeks[0][1]
if current_week_val > 0:
baseline_increase_pct = (
current_week_val - profile.baseline_weekly_km
) / profile.baseline_weekly_km
metrics["baseline_increase_pct"] = baseline_increase_pct
if baseline_increase_pct > 0.6:
if risk_level != "HIGH":
risk_level = "HIGH"
reasons.append(
f"Major spike vs your baseline: {baseline_increase_pct:.1%} above baseline of {profile.baseline_weekly_km}km."
)
if "Reduce volume" not in " ".join(adjustments):
adjustments.append("Reduce volume to closer to your baseline.")
elif baseline_increase_pct > 0.4:
if risk_level == "LOW":
risk_level = "MEDIUM"
reasons.append(
f"Significant increase vs your baseline: {baseline_increase_pct:.1%} above baseline."
)
if "Cap intensity" not in " ".join(adjustments):
adjustments.append(
"Be cautious with this increase; ensure adequate recovery."
)
# 2. Consistency / Monotony Heuristic
# Feature Engineering agent provides consistency_score (0-100)
consistency = features.consistency_score
metrics["consistency_score"] = consistency
if consistency < 30:
if risk_level == "LOW":
risk_level = "MEDIUM"
reasons.append(
f"Low consistency score ({consistency}/100) increases injury risk during ramp-up."
)
adjustments.append("Build base gradually before adding intensity.")
# 3. Pain Reported Flag (from User or scenario)
metrics["pain_reported"] = pain_reported
if pain_reported:
risk_level = "HIGH"
reasons.append("User reported pain or discomfort.")
adjustments.append("Switch all remaining sessions this week to rest or cross-training.")
# 4. Fatigue markers (placeholder - heart rate drift trend if provided)
# In this PoC, we might have pace_trend from trends but it's simple.
return RiskAssessment(
risk_level=risk_level,
reasons=reasons,
recommended_adjustments=adjustments,
metrics_used=metrics,
)
def run(
self,
features: WeeklySummary,
pain_reported: bool = False,
profile: Optional[RunnerProfile] = None,
) -> RiskAssessment:
with obs_logger.start_span("guardrail_agent.run", obs_components.AGENT):
return self.assess_risk(features, pain_reported, profile=profile)