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.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
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.
flowchart LR
CALLER(["Member or Lead"])
subgraph TEL["Telephony"]
SIP["Twilio SIP and PSTN"]
end
subgraph BRAIN["Fitness Studio AI Agent"]
STT["Streaming STT<br/>Deepgram or Whisper"]
NLU{"Intent and<br/>Entity Extraction"}
TOOLS["Tool Calls"]
TTS["Streaming TTS<br/>ElevenLabs or Rime"]
end
subgraph DATA["Live Data Plane"]
CRM[("CRM and Notes")]
CAL[("Calendar and<br/>Schedule")]
KB[("Knowledge Base<br/>and Policies")]
end
subgraph OUT["Outcomes"]
O1(["Class booked"])
O2(["Trial signup captured"])
O3(["Coach handoff"])
end
CALLER --> SIP --> STT --> NLU
NLU -->|Lookup| TOOLS
TOOLS <--> CRM
TOOLS <--> CAL
TOOLS <--> KB
NLU --> TTS --> SIP --> CALLER
NLU -->|Resolved| O1
NLU -->|Schedule| O2
NLU -->|Escalate| O3
style CALLER fill:#f1f5f9,stroke:#64748b,color:#0f172a
style NLU fill:#4f46e5,stroke:#4338ca,color:#fff
style O1 fill:#059669,stroke:#047857,color:#fff
style O2 fill:#0ea5e9,stroke:#0369a1,color:#fff
style O3 fill:#f59e0b,stroke:#d97706,color:#1f2937
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.
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
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
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.