Duward's Project Guide
Project: GPT Workout Planner Category: AI / CLI Last updated: April 18
Note: This guide reflects the latest state of your project repo. It may not match the most up-to-date version if you've worked since.
Where You Are
Solid Checkpoint 1. You have a working CLI: ask for a goal, OpenAI returns a workout plan. Spec is clear, journal has a real entry, uv is set up.
The work this week turns "AI returns something generic" into "AI picks from a list I control, and I remember what it gave me last time." This is also when the project starts showing you what's yours vs what's library code.
Project Structure
Your project splits into two kinds of code:
- Business logic — you handwrite this. The workout categories, how they're loaded, what goes in the prompt, how memory is stored, what counts as valid input. This is what makes your planner different from a plain ChatGPT session.
- Library / view code — agent-assisted is fine. The OpenAI API call itself,
.envsetup withpython-dotenv, the CLI print formatting. Library code that's the same for any project.
Target layout by Thursday:
gpt-workout-planner/
├── main.py ← CLI driver + OpenAI call — agent-assisted OK
├── planner.py ← business logic — handwrite (yours to own)
├── pyproject.toml
├── workouts/
│ ├── categories.txt ← data (new)
│ └── history.txt ← data (new)
└── .env ← secrets (not committed)
Why the split? From Lecture 1: The MVP — on demo day you'll be asked how your planner decides what to suggest. "The AI picks" is a weak answer. "I load these workouts, include them in the prompt, and constrain the AI to pick from the list" is the real answer — and it lives in planner.py.
planner.py should not import openai. That import belongs in main.py.
Phase 1: Create planner.py + Workout Categories File
Handwrite this yourself. The workout list IS your project — what exercises you decide are safe, beginner-friendly, and in scope. And
load_workoutsis your first handwritten business function.
Objective
Move the idea of "what workouts exist" out of the model's head and into your data file, loaded by your code.
Instructions
Sample Data
workouts/categories.txt (one per line: category, name, difficulty):
warm-up, Jumping Jacks, easy
warm-up, Arm Circles, easy
main, Bodyweight Squats, medium
main, Push-Ups, medium
main, Dumbbell Rows, medium
cooldown, Standing Hamstring Stretch, easy
cooldown, Child's Pose, easy
Hints
load_workouts pattern:
def load_workouts(filename):
workouts = []
with open(filename, "r") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
category, name, difficulty = [p.strip() for p in line.split(",")]
workouts.append({
"category": category,
"name": name,
"difficulty": difficulty,
})
return workouts
build_prompt pattern — the key is that the AI can ONLY pick from your list:
def build_prompt(goal, workouts):
workout_list = "\n".join(f"- {w['name']} ({w['category']})" for w in workouts)
return (
"Create a beginner-friendly workout plan.\n"
f"Goal: {goal}\n"
"Use ONLY these workouts:\n"
f"{workout_list}\n"
"Structure: warm-up, main, cooldown. Keep it safe for beginners."
)
Optional — get help from your agent:
Review my load_workouts function in planner.py. Walk me through how dict(zip(header, parts)) style parsing would work as an alternative. Don't change my code — I want to understand the tradeoff.
Phase 2: Memory — Save and Load Past Workouts
Handwrite this yourself. Appending to a history file, reading the last entry — small, but it IS your project's memory model. Type it out.
Objective
After each plan is generated, save it to workouts/history.txt. On startup, offer to show the last one.
Instructions
Sample Output
=== GPT Workout Planner ===
Show your last workout? (y/n): y
--- Last workout (2026-04-17 | goal: build muscle) ---
... the plan ...
What is your workout goal today?
Hints
Appending with a timestamp:
from datetime import date
def save_workout(goal, plan_text):
with open("workouts/history.txt", "a") as f:
f.write(f"\n--- {date.today()} | goal: {goal} ---\n")
f.write(plan_text + "\n")
Reading the last entry:
def load_last_workout():
try:
with open("workouts/history.txt", "r") as f:
text = f.read().strip()
if not text:
return None
entries = text.split("\n---")
return "---" + entries[-1]
except FileNotFoundError:
return None
Phase 3: Validate User Input
Handwrite this yourself. Deciding what's a valid goal is product design, not library code.
Objective
If the user types nothing or "asdf" as a goal, don't waste an API call.
Instructions
Hints
Simple validator:
def is_valid_goal(goal):
goal = goal.strip()
if not goal:
return False
if len(goal) < 3:
return False
if goal.isdigit():
return False
return True
In main():
goal = input("What is your workout goal today? ").strip()
if not is_valid_goal(goal):
print("Please provide a real goal (example: build muscle, lose weight).")
return
Phase 4: Wire It All Together in main.py
Agent-assisted is fine here.
main.pyis the CLI driver: prompts the user, calls OpenAI (library), prints results. The interesting code is already inplanner.py.
Objective
Thin main.py that orchestrates: load workouts → ask goal → validate → call AI with your custom prompt → save to history → print.
Instructions
Hints
Skeleton:
from openai import OpenAI
from planner import load_workouts, build_prompt, save_workout, load_last_workout, is_valid_goal
def generate_workout(prompt):
client = OpenAI() # reads OPENAI_API_KEY from env
response = client.responses.create(model="gpt-4.1-mini", input=prompt)
return response.output_text.strip()
def main():
print("=== GPT Workout Planner ===")
# offer to show last
show_last = input("Show your last workout? (y/n): ").strip().lower()
if show_last == "y":
last = load_last_workout()
print(last if last else "(no history yet)")
# new goal
goal = input("\nWhat is your workout goal today? ").strip()
if not is_valid_goal(goal):
print("Please provide a real goal.")
return
# build prompt from YOUR workouts and call the AI
workouts = load_workouts("workouts/categories.txt")
prompt = build_prompt(goal, workouts)
plan = generate_workout(prompt)
print("\nYour Workout Plan:\n")
print(plan)
save_workout(goal, plan)
if __name__ == "__main__":
main()
Read what you get back. The agent can scaffold this wiring, but you should be able to point at every line and say what it does.
Optional — get help from your agent:
Help me wire up main.py to use the functions in planner.py: load_workouts, build_prompt, save_workout, load_last_workout, is_valid_goal. Keep main.py thin — the OpenAI call is the only real thing it does. Show me the final file.
Phase 5 (Optional): Move the API Key to .env
Agent-assisted is fine here.
python-dotenvsetup is library code
Not urgent (it's the shared class key in a private repo), but good hygiene for any project you'll show off later.
Instructions
Hints
# at the top of main.py, before the OpenAI import usage
from dotenv import load_dotenv
load_dotenv() # reads .env into os.environ
Then OpenAI() with no arguments picks up OPENAI_API_KEY automatically.
Optional — get help from your agent:
Walk me through moving my OpenAI key from main.py to .env using python-dotenv, and adding .env to .gitignore. Show me the exact diff.
Checkpoint 2 Readiness
By Thursday April 23 at 3pm:
Helpful Resources
- Checkpoint 2 Instructions
- Lecture 1: The MVP
- python-dotenv docs — for Phase 5