By Sagar Shankaran, Founder of CallSphere
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.
Key takeaways
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.
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
# 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
# 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
# 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()
# 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)
# 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())
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
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.
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

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
Step-by-step build of a working agent with the OpenAI Agents SDK — Agent class, tools, handoffs, tracing — plus an eval pipeline that catches regressions before merge.
Enterprise CIO Guide perspective on Klarna's AI agent pioneered the resolution-equivalent metric and is now in its third year of production data.
Smolagents lets agents write Python instead of JSON. Why code-as-action reduces tool errors and where the security trade-offs are for production deployments.
SMB Founder Playbook perspective on Klarna's AI agent pioneered the resolution-equivalent metric and is now in its third year of production data.
Healthcare Practice Use Case perspective on Klarna's AI agent pioneered the resolution-equivalent metric and is now in its third year of production data.
Modal turns a Python function into autoscaling serverless compute with optional GPU. Deploy a LiveKit Agent with one command and get pay-per-second billing.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco
Watch how CallSphere handles real customer calls, schedules appointments, and processes payments — live.