runner-ai-intelligence / src /engines /structure_engine.py
avfranco's picture
HF Space deploy snapshot (minimal allow-list)
557ee65
from typing import List, Optional, Literal
from datetime import date, timedelta, datetime
import uuid
from domain.training.run import Run
from domain.training.planned_session import PlannedSession
from domain.runner.goal import Goal
from domain.runner.profile import RunnerProfile
def match_run_to_session(run: Run, sessions: List[PlannedSession]) -> Optional[uuid.UUID]:
"""
Matches a run to a planned session based on date and distance.
Rules:
- abs(run_date - planned_date) <= 1 day
- run_distance >= 0.8 * target_distance
- Match only one session (first match wins)
"""
run_date = run.start_time.date()
run_dist_km = run.total_distance_m / 1000.0
for session in sessions:
if session.completed_run_id is not None:
continue
date_diff = abs((run_date - session.planned_date).days)
if date_diff <= 1 and run_dist_km >= (0.8 * session.target_distance_km):
return session.id
return None
def classify_week(sessions: List[PlannedSession], current_volume: float, goal_volume: float) -> str:
"""
Classifies the week's structure based on completed sessions.
Returns canonical string: strong_week, structured_but_light, rebuild_week, reset_week.
"""
if not sessions:
return "reset_week"
long_run_completed = any(s.session_type == "long_run" and s.completed_run_id for s in sessions)
weekday_sessions = [s for s in sessions if s.session_type == "weekday"]
weekdays_completed = sum(1 for s in weekday_sessions if s.completed_run_id)
# Heuristics
if long_run_completed:
if weekdays_completed >= 2 or current_volume >= goal_volume:
return "strong_week"
else:
return "structured_but_light"
else:
if weekdays_completed >= 2 or (current_volume > 0 and current_volume >= 0.5 * goal_volume):
return "rebuild_week"
else:
return "reset_week"
def generate_week_template(
runner_id: uuid.UUID,
week_start: date,
goal: Optional[Goal],
profile: Optional[RunnerProfile],
previous_sessions: List[PlannedSession] = None,
) -> List[PlannedSession]:
"""
Generates a week template (list of PlannedSession).
Logic:
- Default 3 weekday + 1 long run
- Long run distance based on goal (e.g. 30–40% weekly volume)
- Weekday runs evenly distributed
- Respect baseline weekly distance
"""
# 1. Determine Target Weekly Volume
baseline = profile.baseline_weekly_km if (profile and profile.baseline_weekly_km is not None) else 20.0
target_volume = baseline
if goal and goal.type == "volume" and goal.target_value is not None:
target_volume = goal.target_value
# Simple growth logic if previous week was successful (optional/not requested but good to have)
# For now, stay simple as per requirements.
# 2. Distribute Volume
long_run_dist = target_volume * 0.35
weekday_total = target_volume - long_run_dist
weekday_count = 3
weekday_dist = weekday_total / weekday_count
# 3. Assign Dates
# Mon (0), Tue (1), Wed (2), Thu (3), Fri (4), Sat (5), Sun (6)
# We'll use Tue, Thu, Fri for weekdays and Sun for long run
template_dates = {
1: ("weekday", weekday_dist),
3: ("weekday", weekday_dist),
4: ("weekday", weekday_dist),
6: ("long_run", long_run_dist),
}
sessions = []
for day_offset, (s_type, dist) in template_dates.items():
sessions.append(
PlannedSession(
runner_id=runner_id,
week_start_date=week_start,
session_type=s_type,
planned_date=week_start + timedelta(days=day_offset),
target_distance_km=round(dist, 1),
)
)
return sessions