github-actions[bot] commited on
Commit
c3cf389
·
1 Parent(s): b524c65

🤖 Auto-deploy from GitHub (push) - 45aa088 - 2025-08-05 00:26:50 UTC

Browse files
shared/src/fitness_core/agents/fitness_agent.py CHANGED
@@ -5,7 +5,8 @@ from typing import Optional
5
  from agents import Agent
6
  from dotenv import load_dotenv
7
 
8
- from .models import FitnessPlan, AgentConfig
 
9
  from .providers import ModelProvider
10
  from .tools import get_tool_functions, get_combined_instructions
11
 
@@ -53,7 +54,13 @@ class FitnessAgent(Agent):
53
 
54
  You create personalized plans by iteratively creating plans and asking the user for feedback.
55
 
56
- You provide short, concise responses in conversation.
 
 
 
 
 
 
57
 
58
  {tool_instructions}
59
 
 
5
  from agents import Agent
6
  from dotenv import load_dotenv
7
 
8
+ from .models import AgentConfig
9
+ from .structured_output_models import FitnessPlan
10
  from .providers import ModelProvider
11
  from .tools import get_tool_functions, get_combined_instructions
12
 
 
54
 
55
  You create personalized plans by iteratively creating plans and asking the user for feedback.
56
 
57
+ Create the first plan as soon as the user asks for a fitness plan, and then iterate on it based on their feedback.
58
+
59
+ Never ask for feedback before creating a new plan based on what you already know.
60
+
61
+ You provide short, concise responses in conversation, generally no longer than one or two sentences.
62
+
63
+ Unless specified otherwise, do not respond with any lists or bullet points. Talk like a normal person.
64
 
65
  {tool_instructions}
66
 
shared/src/fitness_core/agents/models.py CHANGED
@@ -1,15 +1,10 @@
1
  """
2
  Pydantic models for the fitness agent.
3
  """
4
- from pydantic import BaseModel
5
  from typing import Optional, List
6
-
7
-
8
- class FitnessPlan(BaseModel):
9
- """Structured fitness plan model."""
10
- name: str
11
- training_plan: str
12
- meal_plan: str
13
 
14
 
15
  class AgentResponse(BaseModel):
 
1
  """
2
  Pydantic models for the fitness agent.
3
  """
4
+ from pydantic import BaseModel, Field
5
  from typing import Optional, List
6
+ from datetime import datetime, date
7
+ from .structured_output_models import FitnessPlan
 
 
 
 
 
8
 
9
 
10
  class AgentResponse(BaseModel):
shared/src/fitness_core/agents/structured_output_models.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Pydantic models for structured LLM outputs.
3
+ """
4
+ from pydantic import BaseModel, Field
5
+ from typing import Optional, List
6
+ from datetime import date
7
+ from enum import Enum
8
+
9
+
10
+ class IntensityLevel(str, Enum):
11
+ """Intensity levels for exercises."""
12
+ LIGHT = "light"
13
+ MODERATE = "moderate"
14
+ HEAVY = "heavy"
15
+ MAX_EFFORT = "max_effort"
16
+
17
+
18
+ class Exercise(BaseModel):
19
+ """Model for a single exercise."""
20
+ name: str = Field(
21
+ description="The name of the exercise (e.g., 'Push-ups', 'Squats', 'Deadlift')"
22
+ )
23
+ description: Optional[str] = Field(
24
+ default=None,
25
+ description="Brief description of how to perform the exercise, including proper form and technique"
26
+ )
27
+ duration: Optional[int] = Field(
28
+ default=None,
29
+ description="Duration of the exercise in seconds (for time-based exercises like planks or cardio)"
30
+ )
31
+ sets: Optional[int] = Field(
32
+ default=None,
33
+ description="Number of sets to perform for this exercise (e.g., 3 sets). Used for strength training and resistance exercises."
34
+ )
35
+ reps: Optional[int] = Field(
36
+ default=None,
37
+ description="Number of repetitions per set (e.g., 10 reps). Used for strength training exercises. Can be a range like '8-12' for variety."
38
+ )
39
+ intensity: Optional[IntensityLevel] = Field(
40
+ default=None,
41
+ description="Intensity level of the exercise. 'light' for warm-up/mobility work, 'moderate' for general training, 'heavy' for strength focus (80-90% effort), 'max_effort' for testing/competition lifts. Can be left blank for rest days or exercises where intensity is not applicable."
42
+ )
43
+
44
+
45
+ class TrainingDay(BaseModel):
46
+ """Model for a single training day."""
47
+ name: str = Field(
48
+ description="Descriptive name for the training day (e.g., 'Upper Body Strength', 'Cardio & Core', 'Active Recovery', 'Rest Day')"
49
+ )
50
+ order_number: int = Field(
51
+ description="The sequential position of this day within the split (1-7 for weekly splits)"
52
+ )
53
+ description: str = Field(
54
+ description="Brief overview of the day's focus and training objectives (e.g., 'Focus on compound upper body movements with moderate intensity')"
55
+ )
56
+ exercises: Optional[List[Exercise]] = Field( # Made optional
57
+ default=None,
58
+ description="List of exercises to be performed on this training day, with full details including sets, reps, and intensity. Leave empty/null for complete rest days."
59
+ )
60
+ intensity: Optional[IntensityLevel] = Field(
61
+ default=None,
62
+ description="Overall intensity of the training day. Use 'light' for recovery/mobility, 'moderate' for standard training, 'heavy' for strength focus, 'max_effort' for testing. Leave blank for complete rest days."
63
+ )
64
+ rest_day: bool = Field(
65
+ default=False,
66
+ description="True if this is a complete rest day with no exercises"
67
+ )
68
+
69
+
70
+ class TrainingPlanSplit(BaseModel):
71
+ """A training split represents a complete cycle of training days (e.g., a weekly routine)."""
72
+ name: str = Field(
73
+ description="Descriptive name for the split approach (e.g., 'Push/Pull/Legs Split', 'Upper/Lower Split', 'Full Body Circuit')"
74
+ )
75
+ order: int = Field(
76
+ description="Sequential order when multiple splits are used in periodization (1 for first phase, 2 for second phase, etc.)"
77
+ )
78
+ description: str = Field(
79
+ description="Detailed explanation of the split's training philosophy, target muscle groups, and how days are organized"
80
+ )
81
+ start_date: Optional[date] = Field(
82
+ default=None,
83
+ description="Optional start date for this specific split. If not provided, will be calculated based on previous splits. Useful for precise scheduling of periodized programs."
84
+ )
85
+ training_days: List[TrainingDay] = Field(
86
+ description="All training days in the split, ordered sequentially. Include rest days for complete weekly schedules."
87
+ )
88
+
89
+
90
+ class TrainingPlan(BaseModel):
91
+ """Complete training plan containing one or more training splits."""
92
+ name: str = Field(
93
+ description="Catchy name that reflects the plan's focus (e.g., 'Beginner Strength Foundation', 'Advanced Powerlifting Prep')"
94
+ )
95
+ description: str = Field(
96
+ description="Comprehensive overview including training philosophy, progression strategy, target audience, and expected timeline"
97
+ )
98
+ training_plan_splits: List[TrainingPlanSplit] = Field(
99
+ description="Ordered list of training splits. Single split for consistent routines, multiple splits for periodized programs with distinct phases (base building → strength → peaking)"
100
+ )
101
+
102
+
103
+ class FitnessPlan(BaseModel):
104
+ """Structured fitness plan model for LLM output."""
105
+ name: str = Field(
106
+ description="Catchy, descriptive name for the fitness plan (e.g., 'Beginner Strength Builder', '30-Day Fat Loss Challenge')"
107
+ )
108
+ goal: str = Field(
109
+ description="Primary goal of the fitness plan (e.g., 'Build muscle', 'Lose weight', 'Improve endurance'). Should be specific and measurable."
110
+ )
111
+ description: str = Field(
112
+ description="Comprehensive overview of the fitness plan, including goals, target audience, and expected outcomes"
113
+ )
114
+ training_plan: TrainingPlan = Field(
115
+ description="The training plan includes all of the workout splits, training days, and exercises"
116
+ )
117
+ meal_plan: str = Field(
118
+ description="Detailed nutrition guidance including meal suggestions, macronutrient targets, and eating schedule. Should be practical and specific to the fitness goals."
119
+ )
120
+ start_date: Optional[date] = Field(
121
+ default_factory=lambda: date.today(),
122
+ description="The date when the user should start this fitness plan"
123
+ )
124
+ target_date: Optional[date] = Field(
125
+ default=None,
126
+ description="Optional target completion date or milestone date for the fitness plan"
127
+ )
shared/src/fitness_core/agents/tools.py CHANGED
@@ -3,9 +3,11 @@ Fitness agent tools with grouped function and prompt definitions.
3
  """
4
  from typing import Optional, Any, Dict, List
5
  from dataclasses import dataclass
 
6
  from agents import function_tool, RunContextWrapper
7
 
8
  from .models import FitnessPlan
 
9
 
10
 
11
  @dataclass
@@ -15,6 +17,123 @@ class FunctionToolConfig:
15
  prompt_instructions: str
16
 
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  @function_tool
19
  async def create_fitness_plan(
20
  ctx: RunContextWrapper[Any],
@@ -32,21 +151,67 @@ async def create_fitness_plan(
32
  # Save the fitness plan to the agent class
33
  FitnessAgent.set_latest_fitness_plan(fitness_plan)
34
 
 
 
 
 
35
  # Format the plan for display
36
  formatted_plan = f"""**{fitness_plan.name}**
37
 
38
- **Training Plan:**
39
- {fitness_plan.training_plan}
 
 
 
 
40
 
41
  **Meal Plan:**
42
  {fitness_plan.meal_plan}"""
43
 
44
- return f"I've created and saved your personalized fitness plan:\n\n{formatted_plan}\n\nThis plan has been tailored specifically for your requirements and is now available in the fitness plan section below. Please consult with a healthcare provider before starting any new fitness program."
 
 
 
 
 
 
 
 
45
 
46
  except Exception as e:
47
  return f"I apologize, but I encountered an error while saving your fitness plan: {str(e)}. Please try again or contact support if the issue persists."
48
 
49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  # Tool configurations with their associated prompt instructions
51
  FITNESS_TOOLS = {
52
  "create_fitness_plan": FunctionToolConfig(
@@ -56,11 +221,27 @@ FITNESS_TOOLS = {
56
 
57
  The FitnessPlan object must include:
58
  - name: A descriptive name for the fitness plan
59
- - training_plan: Detailed training/workout information
 
60
  - meal_plan: Comprehensive nutrition and meal planning details
 
 
 
 
61
 
62
- This tool saves the plan to the FitnessAgent class, makes it available for UI display, and returns a formatted confirmation message.
63
- Do not read the plan back to the user in the conversation as they can see it in the UI component.
 
 
 
 
 
 
 
 
 
 
 
64
  """
65
  )
66
  }
 
3
  """
4
  from typing import Optional, Any, Dict, List
5
  from dataclasses import dataclass
6
+ from datetime import date, timedelta
7
  from agents import function_tool, RunContextWrapper
8
 
9
  from .models import FitnessPlan
10
+ from .structured_output_models import TrainingDay
11
 
12
 
13
  @dataclass
 
17
  prompt_instructions: str
18
 
19
 
20
+ @dataclass
21
+ class ScheduledTrainingDay:
22
+ """A training day with an assigned date."""
23
+ date: date
24
+ training_day: TrainingDay
25
+ split_name: str
26
+ week_number: int
27
+ day_in_week: int
28
+
29
+
30
+ def build_fitness_schedule(fitness_plan: FitnessPlan, start_date: Optional[date] = None) -> List[ScheduledTrainingDay]:
31
+ """
32
+ Build a date-based schedule from a fitness plan.
33
+
34
+ Args:
35
+ fitness_plan: The FitnessPlan object to create a schedule from
36
+ start_date: Optional start date, defaults to fitness_plan.start_date or today
37
+
38
+ Returns:
39
+ List of ScheduledTrainingDay objects with assigned dates
40
+ """
41
+ if start_date is None:
42
+ start_date = fitness_plan.start_date or date.today()
43
+
44
+ # Use target_date as end_date if available
45
+ end_date = fitness_plan.target_date
46
+
47
+ schedule = []
48
+ current_date = start_date
49
+
50
+ # Sort splits by order to ensure proper sequencing
51
+ sorted_splits = sorted(fitness_plan.training_plan.training_plan_splits, key=lambda x: x.order)
52
+
53
+ for split in sorted_splits:
54
+ # Use split's start_date if provided, otherwise use current_date
55
+ split_start_date = split.start_date if split.start_date else current_date
56
+ split_current_date = split_start_date
57
+
58
+ # Calculate how many days this split's cycle is
59
+ days_per_cycle = len(split.training_days)
60
+ week_number = 1
61
+
62
+ # Continue cycling through the split until we reach the end date or run out of splits
63
+ while (end_date is None or split_current_date <= end_date):
64
+ for day_idx, training_day in enumerate(split.training_days):
65
+ # Stop if we've reached the end date
66
+ if end_date and split_current_date > end_date:
67
+ break
68
+
69
+ scheduled_day = ScheduledTrainingDay(
70
+ date=split_current_date,
71
+ training_day=training_day,
72
+ split_name=split.name,
73
+ week_number=week_number,
74
+ day_in_week=day_idx + 1
75
+ )
76
+ schedule.append(scheduled_day)
77
+ split_current_date += timedelta(days=1)
78
+
79
+ # Increment week number after completing a full cycle
80
+ week_number += 1
81
+
82
+ # If this is the last split and we don't have an end date, break after one cycle
83
+ # to avoid infinite loops
84
+ if end_date is None and split == sorted_splits[-1]:
85
+ break
86
+
87
+ # Update current_date for the next split (if no explicit start_date is set for next split)
88
+ current_date = split_current_date
89
+
90
+ return schedule
91
+
92
+
93
+ def format_schedule_summary(schedule: List[ScheduledTrainingDay], days_to_show: int = 14) -> str:
94
+ """
95
+ Format the schedule into a readable summary showing upcoming training days.
96
+
97
+ Args:
98
+ schedule: List of ScheduledTrainingDay objects
99
+ days_to_show: Number of upcoming days to display
100
+
101
+ Returns:
102
+ Formatted string showing the schedule
103
+ """
104
+ if not schedule:
105
+ return "No scheduled training days found."
106
+
107
+ # Show only upcoming days (from today forward)
108
+ today = date.today()
109
+ upcoming_days = [day for day in schedule if day.date >= today][:days_to_show]
110
+
111
+ if not upcoming_days:
112
+ return "No upcoming training days scheduled."
113
+
114
+ summary_lines = ["**Upcoming Training Schedule:**"]
115
+ current_week = None
116
+
117
+ for scheduled_day in upcoming_days:
118
+ # Add week separator
119
+ week_key = f"{scheduled_day.split_name} - Week {scheduled_day.week_number}"
120
+ if week_key != current_week:
121
+ summary_lines.append(f"\n*{week_key}*")
122
+ current_week = week_key
123
+
124
+ # Format the day
125
+ day_str = scheduled_day.date.strftime("%a, %b %d")
126
+ intensity_str = f" ({scheduled_day.training_day.intensity.value})" if scheduled_day.training_day.intensity else ""
127
+
128
+ if scheduled_day.training_day.rest_day:
129
+ summary_lines.append(f"• {day_str}: {scheduled_day.training_day.name}")
130
+ else:
131
+ exercise_count = len(scheduled_day.training_day.exercises) if scheduled_day.training_day.exercises else 0
132
+ summary_lines.append(f"• {day_str}: {scheduled_day.training_day.name}{intensity_str} ({exercise_count} exercises)")
133
+
134
+ return "\n".join(summary_lines)
135
+
136
+
137
  @function_tool
138
  async def create_fitness_plan(
139
  ctx: RunContextWrapper[Any],
 
151
  # Save the fitness plan to the agent class
152
  FitnessAgent.set_latest_fitness_plan(fitness_plan)
153
 
154
+ # Build the schedule from the fitness plan
155
+ schedule = build_fitness_schedule(fitness_plan)
156
+ schedule_summary = format_schedule_summary(schedule)
157
+
158
  # Format the plan for display
159
  formatted_plan = f"""**{fitness_plan.name}**
160
 
161
+ **Goal:** {fitness_plan.goal}
162
+
163
+ **Training Plan:** {fitness_plan.training_plan.name}
164
+ {fitness_plan.training_plan.description}
165
+
166
+ {schedule_summary}
167
 
168
  **Meal Plan:**
169
  {fitness_plan.meal_plan}"""
170
 
171
+ # Calculate duration for display
172
+ if fitness_plan.target_date and fitness_plan.start_date:
173
+ duration_days = (fitness_plan.target_date - fitness_plan.start_date).days
174
+ duration_weeks = duration_days // 7
175
+ duration_text = f"{duration_weeks}-week"
176
+ else:
177
+ duration_text = "customized"
178
+
179
+ return f"I've created and saved your personalized fitness plan with a {duration_text} schedule:\n\n{formatted_plan}\n\nThis plan has been tailored specifically for your requirements and is now available in the fitness plan section below. Please consult with a healthcare provider before starting any new fitness program."
180
 
181
  except Exception as e:
182
  return f"I apologize, but I encountered an error while saving your fitness plan: {str(e)}. Please try again or contact support if the issue persists."
183
 
184
 
185
+ @function_tool
186
+ async def get_training_schedule(
187
+ ctx: RunContextWrapper[Any],
188
+ days_ahead: int = 14,
189
+ ) -> str:
190
+ """Get the upcoming training schedule from the current fitness plan.
191
+
192
+ Args:
193
+ days_ahead: Number of days ahead to show in the schedule (default: 14)
194
+ """
195
+ try:
196
+ # Import here to avoid circular imports
197
+ from .fitness_agent import FitnessAgent
198
+
199
+ # Get the current fitness plan
200
+ fitness_plan = FitnessAgent.get_latest_fitness_plan()
201
+
202
+ if not fitness_plan:
203
+ return "No fitness plan is currently available. Please create a fitness plan first."
204
+
205
+ # Build and format the schedule
206
+ schedule = build_fitness_schedule(fitness_plan)
207
+ schedule_summary = format_schedule_summary(schedule, days_ahead)
208
+
209
+ return f"Here's your upcoming training schedule:\n\n{schedule_summary}"
210
+
211
+ except Exception as e:
212
+ return f"I encountered an error while retrieving your training schedule: {str(e)}. Please try again."
213
+
214
+
215
  # Tool configurations with their associated prompt instructions
216
  FITNESS_TOOLS = {
217
  "create_fitness_plan": FunctionToolConfig(
 
221
 
222
  The FitnessPlan object must include:
223
  - name: A descriptive name for the fitness plan
224
+ - goal: The primary fitness goal
225
+ - training_plan: Detailed training/workout information with splits and days
226
  - meal_plan: Comprehensive nutrition and meal planning details
227
+ - start_date: When the plan should begin (defaults to today)
228
+ - total_duration_weeks: Total duration of the plan
229
+
230
+ This tool automatically builds a date-based schedule and saves the plan to the FitnessAgent class.
231
 
232
+ Do not read the plan back to the user in the conversation. The user can already see it in the UI component.
233
+
234
+ In one or two sentences, let the user know the plan has been created, and ask the user if they want to make any adjustments.
235
+ """
236
+ ),
237
+ "get_training_schedule": FunctionToolConfig(
238
+ function=get_training_schedule,
239
+ prompt_instructions="""
240
+ Use this tool when the user asks about their upcoming workouts, training schedule, or what they should do on specific days.
241
+
242
+ The tool shows the next 14 days by default, but you can specify a different number of days if the user requests it.
243
+
244
+ This tool requires that a fitness plan has already been created.
245
  """
246
  )
247
  }