Build a Fitness Coaching Agent: Workout Planning, Progress Tracking, and Nutrition Advice
Build a complete fitness coaching AI agent that generates personalized workout plans, tracks exercise progress over time, and provides nutrition advice — a personal trainer powered by Python and the OpenAI Agents SDK.
Why Build a Fitness Coaching Agent
Personal trainers cost between fifty and two hundred dollars per hour. Most fitness apps give you static workout templates that ignore your progress, equipment availability, and dietary preferences. A fitness coaching agent bridges this gap: it generates personalized workout plans based on your goals and available equipment, tracks your progress across sessions, adjusts difficulty over time, and provides nutrition advice tailored to your training.
This tutorial builds a complete fitness coaching system with an exercise database, plan generator, progress tracker, and nutrition advisor.
Project Setup
mkdir fitness-agent && cd fitness-agent
python -m venv venv && source venv/bin/activate
pip install openai-agents pydantic
mkdir -p src
touch src/__init__.py src/exercises.py src/planner.py
touch src/progress.py src/nutrition.py src/agent.py
Step 1: Exercise Database
# src/exercises.py
from pydantic import BaseModel
class Exercise(BaseModel):
name: str
muscle_group: str
equipment: str # "none", "dumbbells", "barbell", "machine"
difficulty: str # "beginner", "intermediate", "advanced"
calories_per_set: float
EXERCISES: list[Exercise] = [
Exercise(name="Push-ups", muscle_group="chest",
equipment="none", difficulty="beginner", calories_per_set=8),
Exercise(name="Bench Press", muscle_group="chest",
equipment="barbell", difficulty="intermediate", calories_per_set=10),
Exercise(name="Squats", muscle_group="legs",
equipment="none", difficulty="beginner", calories_per_set=10),
Exercise(name="Barbell Squats", muscle_group="legs",
equipment="barbell", difficulty="intermediate", calories_per_set=14),
Exercise(name="Deadlifts", muscle_group="back",
equipment="barbell", difficulty="advanced", calories_per_set=15),
Exercise(name="Pull-ups", muscle_group="back",
equipment="none", difficulty="intermediate", calories_per_set=9),
Exercise(name="Dumbbell Rows", muscle_group="back",
equipment="dumbbells", difficulty="beginner", calories_per_set=8),
Exercise(name="Shoulder Press", muscle_group="shoulders",
equipment="dumbbells", difficulty="intermediate", calories_per_set=9),
Exercise(name="Plank", muscle_group="core",
equipment="none", difficulty="beginner", calories_per_set=5),
Exercise(name="Lunges", muscle_group="legs",
equipment="none", difficulty="beginner", calories_per_set=8),
Exercise(name="Bicep Curls", muscle_group="arms",
equipment="dumbbells", difficulty="beginner", calories_per_set=6),
Exercise(name="Tricep Dips", muscle_group="arms",
equipment="none", difficulty="intermediate", calories_per_set=7),
Exercise(name="Romanian Deadlifts", muscle_group="legs",
equipment="dumbbells", difficulty="intermediate", calories_per_set=12),
Exercise(name="Lat Pulldown", muscle_group="back",
equipment="machine", difficulty="beginner", calories_per_set=8),
Exercise(name="Leg Press", muscle_group="legs",
equipment="machine", difficulty="beginner", calories_per_set=11),
]
def find_exercises(
muscle_group: str | None = None,
equipment: list[str] | None = None,
difficulty: str | None = None,
) -> list[Exercise]:
results = EXERCISES
if muscle_group:
results = [
e for e in results
if e.muscle_group.lower() == muscle_group.lower()
]
if equipment:
equip_lower = [eq.lower() for eq in equipment]
results = [
e for e in results
if e.equipment.lower() in equip_lower
]
if difficulty:
results = [
e for e in results
if e.difficulty.lower() == difficulty.lower()
]
return results
Step 2: Workout Plan Generator
# src/planner.py
from src.exercises import find_exercises, Exercise
SPLIT_TEMPLATES = {
"full_body": ["chest", "back", "legs", "shoulders", "core", "arms"],
"upper_lower": {
"upper": ["chest", "back", "shoulders", "arms"],
"lower": ["legs", "core"],
},
"push_pull_legs": {
"push": ["chest", "shoulders"],
"pull": ["back", "arms"],
"legs": ["legs", "core"],
},
}
def generate_workout(
split_type: str,
day_name: str,
equipment: list[str],
difficulty: str,
exercises_per_group: int = 2,
) -> str:
if split_type == "full_body":
groups = SPLIT_TEMPLATES["full_body"]
else:
template = SPLIT_TEMPLATES.get(split_type, {})
groups = template.get(day_name.lower(), [])
if not groups:
return f"Invalid split/day combination: {split_type}/{day_name}"
lines = [f"=== {day_name.upper()} DAY ({split_type}) ===\n"]
total_calories = 0.0
for group in groups:
exercises = find_exercises(group, equipment, difficulty)
if not exercises:
exercises = find_exercises(group, ["none"], None)
selected = exercises[:exercises_per_group]
for ex in selected:
sets, reps = _get_sets_reps(difficulty)
cals = ex.calories_per_set * sets
total_calories += cals
lines.append(
f" {ex.name} ({ex.muscle_group})"
)
lines.append(
f" {sets} sets x {reps} reps | "
f"~{cals:.0f} cal | Equipment: {ex.equipment}"
)
lines.append(f"\nEstimated calories burned: {total_calories:.0f}")
return "\n".join(lines)
def _get_sets_reps(difficulty: str) -> tuple[int, int]:
if difficulty == "beginner":
return 3, 10
elif difficulty == "intermediate":
return 4, 10
else:
return 4, 8
Step 3: Progress Tracker
# src/progress.py
from datetime import datetime
from pydantic import BaseModel
class WorkoutLog(BaseModel):
date: str
exercises: dict[str, dict] # name -> {sets, reps, weight}
duration_min: int
notes: str = ""
class ProgressTracker:
def __init__(self):
self.logs: list[WorkoutLog] = []
def log_workout(
self, exercises: dict[str, dict],
duration: int, notes: str = "",
) -> str:
log = WorkoutLog(
date=datetime.now().strftime("%Y-%m-%d"),
exercises=exercises,
duration_min=duration,
notes=notes,
)
self.logs.append(log)
return f"Logged workout: {len(exercises)} exercises, {duration}min"
def get_summary(self, last_n: int = 5) -> str:
if not self.logs:
return "No workouts logged yet."
recent = self.logs[-last_n:]
lines = [f"Last {len(recent)} workouts:\n"]
for log in recent:
lines.append(f"Date: {log.date} | Duration: {log.duration_min}min")
for name, details in log.exercises.items():
lines.append(
f" {name}: {details.get('sets', 0)}x"
f"{details.get('reps', 0)} @ "
f"{details.get('weight', 'bodyweight')}"
)
if log.notes:
lines.append(f" Notes: {log.notes}")
lines.append("")
total_sessions = len(self.logs)
total_time = sum(l.duration_min for l in self.logs)
lines.append(
f"Total: {total_sessions} sessions, {total_time} minutes"
)
return "\n".join(lines)
progress = ProgressTracker()
Step 4: Nutrition Advisor
# src/nutrition.py
MEAL_SUGGESTIONS = {
"muscle_gain": {
"breakfast": "4 eggs, oatmeal with banana, protein shake (600 cal, 45g protein)",
"lunch": "Grilled chicken breast, brown rice, steamed broccoli (650 cal, 50g protein)",
"dinner": "Salmon fillet, sweet potato, mixed greens (600 cal, 40g protein)",
"snacks": "Greek yogurt, almonds, protein bar (400 cal, 30g protein)",
},
"fat_loss": {
"breakfast": "2 eggs, spinach, whole wheat toast (350 cal, 25g protein)",
"lunch": "Turkey wrap with veggies, side salad (400 cal, 35g protein)",
"dinner": "Grilled fish, quinoa, roasted vegetables (450 cal, 35g protein)",
"snacks": "Apple with peanut butter, cottage cheese (250 cal, 15g protein)",
},
"maintenance": {
"breakfast": "3 eggs, toast with avocado, fruit (500 cal, 30g protein)",
"lunch": "Chicken stir fry with rice and vegetables (550 cal, 40g protein)",
"dinner": "Lean steak, baked potato, green beans (550 cal, 40g protein)",
"snacks": "Trail mix, banana, protein shake (350 cal, 25g protein)",
},
}
def get_meal_plan(goal: str) -> str:
goal_key = goal.lower().replace(" ", "_")
plan = MEAL_SUGGESTIONS.get(goal_key)
if not plan:
available = ", ".join(MEAL_SUGGESTIONS.keys())
return f"Unknown goal. Available: {available}"
lines = [f"=== Meal Plan ({goal}) ===\n"]
total_cal = 0
for meal, description in plan.items():
lines.append(f" {meal.title()}: {description}")
cal_str = description.split("(")[1].split(" cal")[0]
total_cal += int(cal_str)
lines.append(f"\nEstimated daily total: ~{total_cal} calories")
return "\n".join(lines)
Step 5: Assemble the Agent
# src/agent.py
import asyncio
import json
from agents import Agent, Runner, function_tool
from src.planner import generate_workout
from src.progress import progress
from src.nutrition import get_meal_plan
@function_tool
def create_workout(
split_type: str = "full_body",
day_name: str = "full_body",
equipment: str = "none",
difficulty: str = "beginner",
) -> str:
"""Generate a workout plan."""
equip_list = [e.strip() for e in equipment.split(",")]
return generate_workout(
split_type, day_name, equip_list, difficulty,
)
@function_tool
def log_exercise(
exercises_json: str, duration_min: int, notes: str = "",
) -> str:
"""Log a completed workout. exercises_json format:
{"Push-ups": {"sets": 3, "reps": 10, "weight": "bodyweight"}}"""
exercises = json.loads(exercises_json)
return progress.log_workout(exercises, duration_min, notes)
@function_tool
def view_progress(last_n: int = 5) -> str:
"""View recent workout history."""
return progress.get_summary(last_n)
@function_tool
def get_nutrition_plan(goal: str) -> str:
"""Get a meal plan for a fitness goal."""
return get_meal_plan(goal)
fitness_agent = Agent(
name="Fitness Coach",
instructions="""You are a personal fitness coaching agent.
Generate workouts based on the user's equipment, experience,
and goals. Track their progress and provide nutrition advice.
Always encourage consistency and progressive overload.
Warn about proper form for advanced exercises.""",
tools=[create_workout, log_exercise, view_progress, get_nutrition_plan],
)
async def main():
result = await Runner.run(
fitness_agent,
"I'm a beginner with dumbbells at home. Create a "
"full body workout and suggest a meal plan for muscle gain.",
)
print(result.final_output)
if __name__ == "__main__":
asyncio.run(main())
FAQ
How does progressive overload work with this agent?
Add a get_personal_records tool that retrieves the user's best weight and reps for each exercise from the progress log. When generating new workouts, the planner checks these records and increases weight by 2.5 to 5 percent or adds one rep. This systematic progression is what drives muscle adaptation over time.
See AI Voice Agents Handle Real Calls
Book a free demo or calculate how much you can save with AI voice automation.
Can the agent adjust workouts based on soreness or injury?
Yes. Add a report_condition tool that takes a muscle group and severity level. The planner then excludes or substitutes exercises targeting that area. For example, if the user reports shoulder soreness, the agent replaces overhead presses with lateral raises or skips shoulder exercises entirely for that session.
How do I make the nutrition advice more precise?
Integrate a food database API like Nutritionix or USDA FoodData Central. Replace the static meal suggestions with calculated macronutrient plans based on the user's body weight, activity level, and goal. The agent can then generate meals that hit specific protein, carb, and fat targets rather than providing generic templates.
#FitnessCoaching #AIAgent #Python #WorkoutPlanning #Nutrition #AgenticAI #LearnAI #AIEngineering
CallSphere Team
Expert insights on AI voice agents and customer communication automation.
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.