| from enum import StrEnum |
| from datetime import date |
| from typing import Literal |
|
|
| from pydantic import BaseModel, ConfigDict, Field, model_validator |
|
|
|
|
| EnergyLevel = Literal["low", "medium", "high"] |
| SleepQuality = Literal["poor", "okay", "good"] |
| PainOrInjury = Literal["yes", "no", "unsure"] |
| PainSeverity = Literal["mild", "moderate", "severe", "unsure"] |
| MoodStress = Literal["stressed", "neutral", "ready"] |
| ContextSignalLabel = Literal[ |
| "recent_endurance_run", |
| "travel", |
| "illness", |
| "alcohol", |
| "unusual_stress", |
| "unusually_hard_work", |
| "other", |
| ] |
| FollowUpField = Literal[ |
| "time_available_minutes", |
| "energy_level", |
| "sleep_quality", |
| "sleep_hours", |
| "soreness", |
| "pain_or_injury", |
| "pain_issues", |
| "mood_stress", |
| "context_signals", |
| "readiness", |
| "other", |
| ] |
|
|
|
|
| class CoachModel(BaseModel): |
| model_config = ConfigDict(extra="forbid") |
|
|
|
|
| class Muscle(StrEnum): |
| PECTORALIS_MAJOR = "pectoralis_major" |
| LATISSIMUS_DORSI = "latissimus_dorsi" |
| TRAPEZIUS = "trapezius" |
| RHOMBOIDS = "rhomboids" |
| FRONT_DELTOID = "front_deltoid" |
| SIDE_DELTOID = "side_deltoid" |
| REAR_DELTOID = "rear_deltoid" |
| BICEPS_BRACHII = "biceps_brachii" |
| BRACHIALIS = "brachialis" |
| TRICEPS_BRACHII = "triceps_brachii" |
| FOREARM_FLEXORS = "forearm_flexors" |
| FOREARM_EXTENSORS = "forearm_extensors" |
| RECTUS_ABDOMINIS = "rectus_abdominis" |
| OBLIQUES = "obliques" |
| SPINAL_ERECTORS = "spinal_erectors" |
| GLUTEUS_MAXIMUS = "gluteus_maximus" |
| GLUTEUS_MEDIUS = "gluteus_medius" |
| QUADRICEPS = "quadriceps" |
| HAMSTRINGS = "hamstrings" |
| ADDUCTORS = "adductors" |
| ABDUCTORS = "abductors" |
| GASTROCNEMIUS = "gastrocnemius" |
| SOLEUS = "soleus" |
|
|
|
|
| class CheckIn(CoachModel): |
| raw_text: str = "" |
| time_available_minutes: int | None = Field(default=None, gt=0) |
| energy_level: EnergyLevel | None = None |
| sleep_quality: SleepQuality | None = None |
| sleep_hours: float | None = Field(default=None, ge=0, le=24) |
| soreness: str = "" |
| pain_or_injury: PainOrInjury = "unsure" |
| pain_issues: list["PainIssue"] = Field(default_factory=list) |
| mood_stress: MoodStress | None = None |
| notes: str = "" |
|
|
|
|
| class PainIssue(CoachModel): |
| affected_muscle: Muscle | None = None |
| severity: PainSeverity = "unsure" |
| notes: str = Field(min_length=1) |
|
|
|
|
| class ContextSignal(CoachModel): |
| label: ContextSignalLabel |
| evidence: str = Field(min_length=1) |
| follow_up_question: str = "" |
|
|
|
|
| class FollowUpQuestion(CoachModel): |
| field: FollowUpField |
| question: str = Field(min_length=1) |
| reason: str = Field(min_length=1) |
|
|
|
|
| class ParsedCheckIn(CoachModel): |
| check_in: CheckIn |
| missing_fields: list[str] = Field(default_factory=list) |
| follow_up_items: list[FollowUpQuestion] = Field(default_factory=list) |
| follow_up_questions: list[str] = Field(default_factory=list) |
| context_signals: list[ContextSignal] = Field(default_factory=list) |
| notes: str = "" |
|
|
|
|
| class Exercise(CoachModel): |
| id: str = Field(min_length=1) |
| name: str = Field(min_length=1) |
| primary_muscle_group: Muscle |
| secondary_muscle_groups: list[Muscle] = Field(default_factory=list) |
| is_compound: bool |
| rep_range_low: int = Field(gt=0) |
| rep_range_high: int = Field(gt=0) |
|
|
| @model_validator(mode="after") |
| def validate_rep_range(self): |
| if self.rep_range_low > self.rep_range_high: |
| raise ValueError("rep_range_low must be less than or equal to rep_range_high") |
| return self |
|
|
|
|
| class PrescribedSet(CoachModel): |
| set_number: int = Field(gt=0) |
| target_reps_low: int = Field(gt=0) |
| target_reps_high: int = Field(gt=0) |
| target_reps: int | None = Field(default=None, gt=0) |
| target_load: float | None = Field(default=None, ge=0) |
| target_rir: int | None = Field(default=None, ge=0) |
|
|
| @model_validator(mode="after") |
| def validate_target_rep_range(self): |
| if self.target_reps_low > self.target_reps_high: |
| raise ValueError("target_reps_low must be less than or equal to target_reps_high") |
| if self.target_reps is not None and not ( |
| self.target_reps_low <= self.target_reps <= self.target_reps_high |
| ): |
| raise ValueError("target_reps must be inside the target rep range") |
| return self |
|
|
|
|
| class PlannedExercise(CoachModel): |
| exercise_id: str = Field(min_length=1) |
| order: int = Field(gt=0) |
| prescribed_sets: list[PrescribedSet] = Field(min_length=1) |
| rest_seconds: int | None = Field(default=None, ge=0) |
| notes: str = "" |
|
|
|
|
| class SessionPlan(CoachModel): |
| date: date |
| check_in: CheckIn |
| planned_exercises: list[PlannedExercise] = Field(min_length=1) |
| notes: str = "" |
|
|
|
|
| class LoggedSet(CoachModel): |
| set_number: int = Field(gt=0) |
| actual_reps: int = Field(gt=0) |
| actual_load: float = Field(ge=0) |
| rpe: float | None = Field(default=None, ge=0, le=10) |
| rest_seconds_before: int | None = Field(default=None, ge=0) |
| notes: str = "" |
|
|
|
|
| class LoggedExercise(CoachModel): |
| exercise_id: str = Field(min_length=1) |
| order: int = Field(gt=0) |
| logged_sets: list[LoggedSet] = Field(min_length=1) |
| notes: str = "" |
|
|
|
|
| class SessionLog(CoachModel): |
| date: date |
| planned_session: SessionPlan |
| logged_exercises: list[LoggedExercise] = Field(min_length=1) |
| notes: str = "" |
|
|
|
|
| class CompletedSet(CoachModel): |
| exercise_id: str = Field(min_length=1) |
| set_number: int = Field(gt=0) |
| actual_reps: int = Field(gt=0) |
| actual_load: float = Field(ge=0) |
| rpe: float | None = Field(default=None, ge=0, le=10) |
| notes: str = "" |
|
|
|
|
| class CompletedSession(CoachModel): |
| date: date |
| day_number: int = Field(ge=1, le=4) |
| completed_sets: list[CompletedSet] = Field(min_length=1) |
| notes: str = "" |
|
|