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, .env setup with python-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_workouts is 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.py is the CLI driver: prompts the user, calls OpenAI (library), prints results. The interesting code is already in planner.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-dotenv setup 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