AI Fitness Coach with RAG
Build a RAG pipeline that generates personalized fitness plans with an LLM.
Introduction
30 Second Summary
You know how every fitness app gives the same generic advice? "Eat clean and work out more" is not a plan. Real coaches build programs from your specific numbers and back them with nutrition science.
In this project, you will build FitGenAI, a personalized AI fitness coach that calculates your exact calorie and macronutrient targets, then generates a 7-day meal plan, workout schedule, and grocery list grounded in real nutrition guidelines.
What You'll Build
You fill in a form with your fitness profile, hit generate, and watch a complete personalized fitness plan appear on screen. You can open it on your phone via a shareable link and show it to anyone.
By the end of this project, you'll have:
- A working fitness calculation engine that takes your age, weight, height, and activity level and instantly displays your BMR, TDEE, daily calories, protein/fat/carb targets, and water intake.
- A RAG pipeline that retrieves evidence-based guidelines (WHO, USDA, ACSM, ISSN) from a vector database and feeds them to an LLM so every recommendation cites its source instead of guessing.
- A shareable Gradio web dashboard with tabs for your metrics, 7-day meal plan, workout schedule, and grocery list. Accessible from any device via a public link.
- Secret Mission: Add an AI chatbot tab that answers follow-up fitness questions using the same RAG pipeline.
Are there any prerequisites?
You need a Google account (for Google Colab access) and a free Groq account (signup at console.groq.com). No local installs are required. Everything runs in your browser.
Before We Start
Before We Start...
Before we dive in, let's lock in what you're about to build and why it matters. FitGenAI isn't just another chatbot that guesses at nutrition advice. It grounds every recommendation in real science from organizations like WHO, USDA, ACSM, and ISSN.
Install Packages and Connect to Groq
FitGenAI needs four core tools: a vector database to store fitness knowledge, an embedding model to turn text into searchable vectors, a UI framework for the dashboard, and an LLM API for generating plans.
In this step, you will install all four packages in your Colab notebook and verify that your Groq connection works by sending a test message to Llama 3.3 70B.
In this step, get ready to:
- Install ChromaDB, Sentence Transformers, Gradio, and the Groq SDK.
- Store your Groq API key securely as a Colab secret.
- Send a test message to Groq and confirm a response prints in the output.
Install the required Python packages
Google Colab comes with many libraries pre-installed, but you need four additional packages for this project.
- In your Colab notebook, click into the first empty code cell and install all packages by running this command:
!pip install chromadb sentence-transformers gradio groq
What does this command do?
- chromadb is a vector database that stores and searches your fitness knowledge base.
- sentence-transformers provides the embedding model that converts text into numerical vectors.
- gradio builds web UIs in pure Python with shareable links.
- groq is Groq's Python SDK for calling Llama 3.3 70B.
- Run the cell by pressing Shift+Enter.
- Wait for the install to complete. You will see several lines of output ending with Successfully installed followed by package names.
Seeing a dependency error?
Colab occasionally has version conflicts with pre-installed packages. Try adding --quiet to suppress warnings, or restart the runtime from the menu (Runtime > Restart runtime) and re-run the cell.
help me fix a pip install error in Google Colab
Store your Groq API key as a Colab secret
Your API key connects the notebook to Groq's LLM inference service. Colab's Secrets panel keeps it out of your code so you never accidentally share it.
- In the left sidebar of your Colab notebook, click the key icon (Secrets) to open the Secrets panel.
- Click Add new secret.
- In the Name field, type GROQ_API_KEY.
- In the Value field, paste the API key you copied from console.groq.com.
- Make sure the toggle next to Notebook access is switched on. This allows your code cells to read the secret.
Where do I find my Groq API key?
Sign in at console.groq.com and navigate to the API Keys section. If you have not created a key yet, click Create API Key, give it a name, and copy the key value. You will only see the full key once.
Don't see the Secrets panel?
If the left sidebar is collapsed, click the arrow at the far left edge of the screen to expand it. The key icon appears below the folder icon.
help me find the Secrets panel in Google Colab
Import libraries and test your Groq connection
Now you will write the code that imports every library the project needs and sends a quick test message to Groq. If the message comes back, your entire setup is confirmed.
- Click the + Code button to create a new cell below Cell 1, then paste the following code:
import os
import json
from google.colab import userdata
from groq import Groq
import chromadb
from sentence_transformers import SentenceTransformer
import gradio as gr
# Read the API key from Colab Secrets
GROQ_API_KEY = userdata.get("GROQ_API_KEY")
client = Groq(api_key=GROQ_API_KEY)
# Test function to verify the connection works
def test_groq():
chat_completion = client.chat.completions.create(
messages=[{"role": "user", "content": "Say hello in one sentence."}],
model="llama-3.3-70b-versatile",
)
return chat_completion.choices[0].message.content
print("Groq connection test:", test_groq())
What does this code do?
- userdata.get("GROQ_API_KEY") reads the secret you stored in the Secrets panel. Your key never appears in the notebook itself.
- Groq(api_key=GROQ_API_KEY) creates a client that talks to Groq's API.
- test_groq() sends a one-sentence prompt to the llama-3.3-70b-versatile model and returns its response.
- The print line at the bottom calls test_groq() immediately so you can see the result when you run the cell.
Before you run this cell, what do you expect to see printed in the output?
- Run the cell by pressing Shift+Enter.
You should see output like Groq connection test: Hello! How can I assist you today? (the exact wording varies on each run). The key thing is a greeting message appears with no errors.
Seeing a SecretNotFoundError?
This means Colab cannot find a secret named GROQ_API_KEY. Double-check that you typed the name exactly (all uppercase, underscores) in the Secrets panel and that the notebook access toggle is switched on.
help me fix a SecretNotFoundError in Google Colab
Seeing an AuthenticationError or 401?
This means the API key value itself is invalid or expired. Go back to console.groq.com, confirm your key is active, and paste a fresh copy into the Secrets panel.
help me fix a Groq AuthenticationError
✔️ Awesome, I've got everything!
Great. Make sure both cells are saved in your notebook before moving on.
ⓧ I'd like to double check the full code
Here is the complete code for each cell. Compare yours against these:
Cell 1:
!pip install chromadb sentence-transformers gradio groq
Cell 2:
import os
import json
from google.colab import userdata
from groq import Groq
import chromadb
from sentence_transformers import SentenceTransformer
import gradio as gr
GROQ_API_KEY = userdata.get("GROQ_API_KEY")
client = Groq(api_key=GROQ_API_KEY)
def test_groq():
chat_completion = client.chat.completions.create(
messages=[{"role": "user", "content": "Say hello in one sentence."}],
model="llama-3.3-70b-versatile",
)
return chat_completion.choices[0].message.content
print("Groq connection test:", test_groq())
Your environment is locked in. Every package is installed, your API key is secure, and Groq is responding. Next up, you will build the fitness calculation engine and a live form that shows personalized numbers the moment you submit your profile.
Build the User Profile Form and Fitness Calculations
Your Colab environment is set up and Groq is responding. Now it is time to build something you can actually see and interact with.
A personalized fitness coach needs two things: accurate calculations based on nutrition science, and a way for users to input their details. In this step, you will build the calculation engine (BMR, TDEE, macros, water intake) and wrap it in a Gradio web form that displays results instantly.
In this step, get ready to:
- Build fitness calculation functions using the Mifflin-St Jeor equation.
- Create a Gradio Blocks interface with user profile input fields.
- Wire the form to the calculations and verify output on screen.
Build the fitness calculation functions
Every recommendation your AI coach makes depends on accurate calorie and macronutrient targets. These targets come from well-established formulas in sports nutrition.
- Create a new code cell below your Cell 2 (Imports and Groq Setup). Paste the following functions into it:
def calculate_bmr(weight_kg, height_cm, age, gender):
if gender == "Male":
return 10 * weight_kg + 6.25 * height_cm - 5 * age + 5
else:
return 10 * weight_kg + 6.25 * height_cm - 5 * age - 161
def calculate_tdee(bmr, activity_level):
multipliers = {
"Sedentary (little or no exercise)": 1.2,
"Lightly active (1-3 days/week)": 1.375,
"Moderately active (3-5 days/week)": 1.55,
"Very active (6-7 days/week)": 1.725,
"Extra active (athlete/physical job)": 1.9,
}
return bmr * multipliers.get(activity_level, 1.55)
def calculate_daily_calories(tdee, fitness_goal):
if fitness_goal == "Weight Loss":
return tdee - 500
elif fitness_goal == "Muscle Gain":
return tdee + 300
else:
return tdee
What do these functions do?
calculate_bmr uses the Mifflin-St Jeor equation to estimate how many calories your body burns at complete rest. The formula differs slightly for males and females.
calculate_tdee multiplies BMR by an activity factor (ranging from 1.2 for sedentary to 1.9 for athletes) to estimate total daily energy expenditure.
calculate_daily_calories adjusts TDEE based on the user's goal: subtract 500 kcal for weight loss, add 300 kcal for muscle gain, or keep it the same for maintenance.
- Add these functions directly below in the same cell:
def calculate_macros(daily_calories, weight_kg):
protein_g = weight_kg * 2.0
protein_cal = protein_g * 4
fat_cal = daily_calories * 0.25
fat_g = fat_cal / 9
carb_cal = daily_calories - protein_cal - fat_cal
carb_g = carb_cal / 4
return round(protein_g), round(fat_g), round(carb_g)
def calculate_water_intake(weight_kg):
return round(weight_kg * 35 / 1000, 1)
def get_all_metrics(age, gender, height_cm, weight_kg, activity_level, fitness_goal):
bmr = calculate_bmr(weight_kg, height_cm, age, gender)
tdee = calculate_tdee(bmr, activity_level)
daily_calories = calculate_daily_calories(tdee, fitness_goal)
protein_g, fat_g, carb_g = calculate_macros(daily_calories, weight_kg)
water_l = calculate_water_intake(weight_kg)
return {
"bmr": round(bmr),
"tdee": round(tdee),
"daily_calories": round(daily_calories),
"protein_g": protein_g,
"fat_g": fat_g,
"carb_g": carb_g,
"water_l": water_l,
}
What does this code do?
calculate_macros splits daily calories into protein (2g per kg bodyweight), fat (25% of calories), and carbs (whatever remains). These ratios follow ISSN sports nutrition guidelines.
calculate_water_intake uses the EFSA recommendation of 35ml per kg bodyweight per day.
get_all_metrics is the aggregator. It chains all calculations together and returns a dictionary with every metric the app needs.
- Run Cell 3 by clicking the play button (or pressing Shift+Enter).
The cell executes with no output and no errors. This confirms all six functions are defined and ready to use.
Seeing a syntax error?
Check that both code chunks are in the same cell, with no extra indentation or missing colons. The most common mistake is a stray space before def on the second chunk.
help me fix a syntax error in my fitness calculation functions
✔️ Awesome, I've got everything!
Great. Double check you see no errors when the cell runs, then move on to building the form.
ⓧ I'd like to double check the full code
Your Cell 3 should contain all six functions together. Scroll up and confirm you have calculate_bmr, calculate_tdee, calculate_daily_calories, calculate_macros, calculate_water_intake, and get_all_metrics present in that order, with no gaps or duplicates.
Create the Gradio profile form and wire it to calculations
Now you will build the interface users interact with. Gradio lets you create full web UIs in pure Python, and in Colab it generates a public link so you can open the app on any device.
- Create a new code cell below Cell 3. Paste the following function that processes a submitted profile and returns formatted metrics:
user_metrics_store = {}
def process_profile(age, gender, height_cm, weight_kg, activity_level, fitness_goal,
target_duration, gym_days, food_preference, allergies, medical_conditions, budget):
metrics = get_all_metrics(age, gender, height_cm, weight_kg, activity_level, fitness_goal)
user_metrics_store["metrics"] = metrics
user_metrics_store["profile"] = f"{age}yo {gender}, {height_cm}cm, {weight_kg}kg, {activity_level}"
metrics_display = f"""## Your Fitness Metrics
| Metric | Value |
|--------|-------|
| BMR (Basal Metabolic Rate) | {metrics['bmr']} kcal/day |
| TDEE (Total Daily Energy Expenditure) | {metrics['tdee']} kcal/day |
| Daily Calorie Target | {metrics['daily_calories']} kcal/day |
| Protein | {metrics['protein_g']}g/day |
| Fat | {metrics['fat_g']}g/day |
| Carbohydrates | {metrics['carb_g']}g/day |
| Water Intake | {metrics['water_l']}L/day |
"""
return metrics_display
What does process_profile do?
This function calls get_all_metrics with the form inputs, stores the results in user_metrics_store (so later steps can access them), and builds a Markdown table showing every calculated value.
- Add the Gradio interface code below in the same cell. This builds the left column of input fields:
with gr.Blocks(title="FitGenAI - AI Fitness Coach", theme=gr.themes.Soft()) as demo:
gr.Markdown("# FitGenAI - Your AI-Powered Fitness Coach\nFill in your profile to get personalized, evidence-based fitness recommendations.")
with gr.Tab("Profile & Metrics"):
with gr.Row():
with gr.Column():
age = gr.Number(label="Age", value=25, minimum=15, maximum=80)
gender = gr.Dropdown(label="Gender", choices=["Male", "Female"], value="Male")
height_cm = gr.Number(label="Height (cm)", value=175, minimum=100, maximum=250)
weight_kg = gr.Number(label="Weight (kg)", value=75, minimum=30, maximum=200)
activity_level = gr.Dropdown(
label="Activity Level",
choices=[
"Sedentary (little or no exercise)",
"Lightly active (1-3 days/week)",
"Moderately active (3-5 days/week)",
"Very active (6-7 days/week)",
"Extra active (athlete/physical job)",
],
value="Moderately active (3-5 days/week)"
)
- Continue adding this code directly below in the same cell. This builds the right column and wires the button:
with gr.Column():
fitness_goal = gr.Dropdown(
label="Fitness Goal",
choices=["Weight Loss", "Muscle Gain", "Maintenance"],
value="Muscle Gain"
)
target_duration = gr.Dropdown(label="Target Duration", choices=["4 weeks", "8 weeks", "12 weeks", "16 weeks"], value="12 weeks")
gym_days = gr.Dropdown(label="Preferred Gym Days/Week", choices=["2", "3", "4", "5", "6"], value="4")
food_preference = gr.Dropdown(label="Food Preference", choices=["Non-Vegetarian", "Vegetarian", "Vegan"], value="Non-Vegetarian")
allergies = gr.Textbox(label="Allergies (comma-separated)", placeholder="e.g., peanuts, dairy")
medical_conditions = gr.Textbox(label="Medical Conditions", placeholder="e.g., diabetes, hypertension")
budget = gr.Dropdown(label="Budget", choices=["Low", "Medium", "High"], value="Medium")
calc_btn = gr.Button("Calculate My Metrics", variant="primary")
metrics_output = gr.Markdown()
calc_btn.click(
fn=process_profile,
inputs=[age, gender, height_cm, weight_kg, activity_level, fitness_goal,
target_duration, gym_days, food_preference, allergies, medical_conditions, budget],
outputs=metrics_output
)
demo.launch(share=True)
What does this interface do?
gr.Blocks creates a full dashboard layout. The two gr.Column() blocks arrange your inputs side by side. The dropdowns pre-fill sensible defaults so the learner can test immediately.
calc_btn.click connects the button to process_profile. When clicked, Gradio passes all 12 inputs to the function and renders the returned Markdown in metrics_output.
demo.launch(share=True) starts a local server and creates a public URL (like https://xxxxx.gradio.live) so you can open the app from any device.
Before you run this cell, what do you think will appear in the cell output?
- Run Cell 4 by pressing Shift+Enter.
You will see Gradio print a local URL and a public URL ending in .gradio.live. The Gradio interface also renders directly in the Colab output area.
- Leave the default values in the form (25-year-old male, 175cm, 75kg, moderately active, muscle gain).
- Click the Calculate My Metrics button.
A table appears showing your calculated metrics. For the default profile, you should see a BMR around 1723 kcal, TDEE around 2671 kcal, and a daily calorie target around 2971 kcal (TDEE + 300 for muscle gain).
Not seeing the Gradio interface?
Make sure your Cell 4 contains all three chunks in sequence: the process_profile function, then the with gr.Blocks(...) block, then demo.launch(share=True) at the bottom. A missing demo.launch() call means nothing renders.
If you see an indentation error, check that the with gr.Column(): blocks are indented consistently. Every line inside a with block must be indented one level deeper.
help me fix my Gradio interface that is not rendering in Colab
Why Gradio instead of Flask?
Gradio builds full web UIs in pure Python. No HTML, CSS, or JavaScript required. It works natively in Colab with automatic share links, making it ideal for ML demos where the focus is on the model, not frontend code.
Your calculation engine is working and the form displays real personalized numbers. Next, you will send these metrics to the LLM and see what happens when you ask for a meal plan without any domain knowledge backing it up.
Ask the LLM Without RAG
Your fitness calculations are working. You can turn someone's age, weight, and activity level into precise calorie and macro targets.
Now comes the natural next question: can an LLM turn those numbers into a personalized meal plan and workout schedule? In this step, you'll send your user's profile straight to Groq and see what comes back. The result will be eye-opening for the wrong reasons.
In this step, get ready to:
- Write a prompt template that feeds user metrics to the LLM.
- Send the prompt to Llama 3.3 70B and read the generated plan.
- Observe why ungrounded LLM output falls short for fitness advice.
Write the prompt template function
The simplest approach is to hand the LLM everything it needs in a single prompt: the user's calorie target, macro breakdown, and a request for a meal plan and workout. No external knowledge, no retrieval. Just raw LLM generation.
- Create a new code cell in your Colab notebook (below your calculation functions) and paste the following function:
def generate_without_rag(metrics, user_profile):
prompt = f"""Based on this user profile, generate a brief meal plan and workout schedule:
User: {user_profile}
Daily Calories: {metrics['daily_calories']} kcal
Protein: {metrics['protein_g']}g | Fat: {metrics['fat_g']}g | Carbs: {metrics['carb_g']}g
Generate a 1-day sample meal plan and 1-day workout plan."""
# Send the prompt to Groq with no additional context
response = client.chat.completions.create(
messages=[{"role": "user", "content": prompt}],
model="llama-3.3-70b-versatile",
temperature=0.7,
max_tokens=1500,
)
return response.choices[0].message.content
What does this code do?
This function takes the user's calculated metrics and a text summary of their profile, then asks the LLM to generate a plan based only on those numbers.
- The prompt includes calorie and macro targets but gives the LLM no source material to reference.
- temperature=0.7 adds some creativity to the response so it doesn't repeat identical meals every time.
- max_tokens=1500 caps the response length to keep it concise for this initial test.
- Run the cell to define the function.
Send a test profile to the LLM
Now you'll call the function with a sample profile and read what the LLM generates. Before you run it, think about what you expect: will the LLM produce a plan that cites real nutrition guidelines? Will it stick precisely to the calorie targets you calculated?
- Create a new cell below and test the function with a sample profile by running this code:
# Test with a sample user profile
test_metrics = get_all_metrics(25, "Male", 175, 75, "Moderately active (3-5 days/week)", "Muscle Gain")
test_profile = "25yo Male, 175cm, 75kg, Moderately active (3-5 days/week), goal: Muscle Gain"
print("=== LLM Response WITHOUT RAG ===")
print(f"Target: {test_metrics['daily_calories']} kcal/day")
print(f"Macros: {test_metrics['protein_g']}g protein, {test_metrics['fat_g']}g fat, {test_metrics['carb_g']}g carbs")
print("\n" + "="*50 + "\n")
print(generate_without_rag(test_metrics, test_profile))
What does this test do?
This creates a 25-year-old male profile with moderate activity aiming for muscle gain, then prints the calculated targets alongside the LLM's response. Printing both lets you compare whether the LLM's meal plan actually matches your computed numbers.
- Run the cell and read through the generated meal plan and workout schedule.
Seeing a rate limit error?
Groq's free tier may return a 429 error if you've sent too many requests in a short period. Wait 30-60 seconds and run the cell again.
If you see an authentication error, double check that your GROQ_API_KEY secret is still set in Colab's sidebar.
Help me debug my Groq API call in Colab
Observe the limitations
You should see a plausible-sounding meal plan with breakfast, lunch, dinner, and snacks. The LLM might even list approximate calories per meal. It looks helpful at first glance.
Now read it critically. This is the designed shortfall you need to see before building the fix.
- Check whether the response cites any specific guidelines (WHO, USDA, ACSM, ISSN). It does not.
- Add up the calories from the suggested meals. They likely don't match your calculated target precisely.
- Look for any evidence basis. The LLM is guessing from its training data, not consulting real nutrition science.
Why does this matter?
The LLM produces generic advice because it has no access to domain-specific knowledge at generation time. It cannot cite WHO protein guidelines, USDA dietary patterns, or ACSM exercise recommendations because those documents are not in its prompt.
This is the core problem that Retrieval-Augmented Generation solves. Instead of relying on the LLM's training memory, you retrieve the exact guidelines relevant to a user's profile and inject them into the prompt. The next step builds that pipeline.
✔️ Awesome, I've got everything!
Great. Make sure you've saved all cells in your Colab notebook.
ⓧ I'd like to double check the full code
Here's the complete code for the new cell you added in this step:
def generate_without_rag(metrics, user_profile):
prompt = f"""Based on this user profile, generate a brief meal plan and workout schedule:
User: {user_profile}
Daily Calories: {metrics['daily_calories']} kcal
Protein: {metrics['protein_g']}g | Fat: {metrics['fat_g']}g | Carbs: {metrics['carb_g']}g
Generate a 1-day sample meal plan and 1-day workout plan."""
# Send the prompt to Groq with no additional context
response = client.chat.completions.create(
messages=[{"role": "user", "content": prompt}],
model="llama-3.3-70b-versatile",
temperature=0.7,
max_tokens=1500,
)
return response.choices[0].message.content
You've seen the gap firsthand. The LLM gives you plausible-sounding advice, but it's untethered from real guidelines. In the next step, you'll build a knowledge base of evidence-based fitness and nutrition documents, embed them into a vector database, and retrieve exactly the right context for each user's profile.
Build the RAG Knowledge Base
In the last step, you saw the LLM generate plausible-sounding fitness advice with no sources and no real adherence to your calorie targets. The problem is clear: without domain-specific knowledge, the model has nothing to ground its recommendations in.
Now you fix that gap. You will build a fitness knowledge base containing real guidelines from WHO, USDA, ACSM, ISSN, and EFSA, then store them as vector embeddings in ChromaDB so the system can retrieve exactly the right context for each user's profile.
In this step, get ready to:
- Create a knowledge base of 10 evidence-based fitness and nutrition documents.
- Index those documents into ChromaDB using Sentence Transformers embeddings.
- Write a retrieval function and verify it returns relevant results for a test query.
Create the fitness knowledge documents
A RAG knowledge base is a collection of text chunks the system can search through. Each chunk covers a specific topic and carries metadata so you know where the information came from.
- Create a new code cell in your Colab notebook (below the existing cells).
- Define the fitness_knowledge list by pasting this code into the new cell:
fitness_knowledge = [
{"id": "who_protein", "text": "WHO recommends 0.83g protein per kg body weight for sedentary adults. For active individuals and muscle gain, research supports 1.6-2.2g per kg body weight (ISSN Position Stand, 2017). Higher intakes up to 2.2g/kg show no adverse effects in healthy adults and support muscle protein synthesis during resistance training.", "metadata": {"source": "WHO/ISSN", "category": "protein"}},
{"id": "usda_dietary", "text": "USDA Dietary Guidelines recommend 45-65% of calories from carbohydrates, 20-35% from fats, and 10-35% from protein. For weight management, a moderate deficit of 500 kcal/day leads to approximately 0.45kg (1 lb) fat loss per week. Whole grains, fruits, vegetables, lean proteins, and healthy fats should form the foundation of meals.", "metadata": {"source": "USDA", "category": "diet"}},
{"id": "acsm_exercise", "text": "ACSM recommends 150-300 minutes of moderate-intensity or 75-150 minutes of vigorous-intensity aerobic exercise per week for adults. Resistance training should target all major muscle groups 2-3 days per week with 48 hours recovery between sessions for the same muscle group. Beginners should start with 2-3 sets of 8-12 repetitions.", "metadata": {"source": "ACSM", "category": "exercise"}},
{"id": "issn_meal_timing", "text": "ISSN Position Stand on Nutrient Timing: Distributing protein intake across 4-5 meals of 20-40g per serving optimizes muscle protein synthesis. Pre and post-workout nutrition within 2 hours of training supports recovery. Total daily protein intake is more important than exact timing for muscle growth.", "metadata": {"source": "ISSN", "category": "meal_timing"}},
{"id": "hydration_guidelines", "text": "General hydration recommendation is 35ml per kg body weight per day (European Food Safety Authority). Active individuals may need 500-1000ml additional water per hour of exercise. Signs of adequate hydration include pale yellow urine color. Electrolyte replacement is recommended for exercise lasting over 60 minutes.", "metadata": {"source": "EFSA", "category": "hydration"}},
{"id": "weight_loss_principles", "text": "Evidence-based weight loss principles: A caloric deficit of 500-750 kcal/day is recommended for sustainable fat loss (1-1.5 lbs per week). Protein should be kept high (1.6-2.4g/kg) during a deficit to preserve lean mass. Resistance training 3-4x per week prevents muscle loss. Crash diets below BMR are counterproductive and lead to metabolic adaptation.", "metadata": {"source": "ACSM/ISSN", "category": "weight_loss"}},
{"id": "muscle_gain_principles", "text": "For muscle hypertrophy: A caloric surplus of 200-500 kcal above TDEE supports lean muscle gain while minimizing fat accumulation. Progressive overload (increasing weight, reps, or sets over time) is the primary driver of muscle growth. Training each muscle group 2x per week with 10-20 sets per muscle group per week is optimal for hypertrophy.", "metadata": {"source": "ACSM/ISSN", "category": "muscle_gain"}},
{"id": "vegetarian_nutrition", "text": "Vegetarian and vegan athletes can meet all nutritional needs with proper planning. Key considerations: combine plant proteins for complete amino acid profiles (legumes + grains), supplement vitamin B12, ensure adequate iron intake from lentils/spinach/fortified foods, and consider creatine supplementation as plant diets are naturally low in creatine. Protein sources include tofu (17g/100g), lentils (9g/100g cooked), chickpeas (19g/100g), tempeh (20g/100g).", "metadata": {"source": "ISSN", "category": "vegetarian"}},
{"id": "workout_splits", "text": "Common workout splits by training frequency: 3 days/week - Full body (compound movements each session). 4 days/week - Upper/Lower split (2 upper, 2 lower days). 5-6 days/week - Push/Pull/Legs (each trained 2x). Beginners benefit most from full body routines focusing on compound lifts: squats, deadlifts, bench press, rows, overhead press.", "metadata": {"source": "ACSM", "category": "workout_programming"}},
{"id": "budget_nutrition", "text": "Budget-friendly high-protein foods: eggs, canned tuna, chicken thighs, lentils, beans, oats, rice, frozen vegetables, peanut butter, greek yogurt, cottage cheese. Meal prep strategies: cook proteins and grains in bulk on weekends, use versatile sauces for variety, buy seasonal produce, and prioritize calorie-dense staples (rice, oats, potatoes) for affordable energy.", "metadata": {"source": "General Nutrition", "category": "budget"}},
]
What does this code do?
Each dictionary in the list represents one knowledge document with three fields:
- id gives the document a unique identifier ChromaDB uses to track it.
- text is the actual content chunk that gets embedded and searched.
- metadata tags the source (WHO, USDA, ACSM, etc.) and category so you can filter results later.
Initialize ChromaDB and load the documents
With the knowledge documents defined, you now need a way to search them by meaning rather than by keywords. Sentence Transformers converts each text chunk into a numeric vector (an embedding), and ChromaDB stores those vectors so you can find the closest matches to any query.
- Below the fitness_knowledge list in the same cell, add the ChromaDB initialization code:
embedding_model = SentenceTransformer("all-MiniLM-L6-v2")
chroma_client = chromadb.Client()
collection = chroma_client.get_or_create_collection(
name="fitness_knowledge",
metadata={"hnsw:space": "cosine"}
)
collection.add(
ids=[doc["id"] for doc in fitness_knowledge],
documents=[doc["text"] for doc in fitness_knowledge],
metadatas=[doc["metadata"] for doc in fitness_knowledge],
)
What does this code do?
SentenceTransformer("all-MiniLM-L6-v2") loads a lightweight embedding model that runs locally in Colab. It converts text into 384-dimensional vectors for free, with no API calls needed.
chromadb.Client() creates an in-memory vector database. The collection uses cosine similarity ("hnsw:space": "cosine") to measure how semantically close two text chunks are.
collection.add() inserts all 10 documents with their IDs, text content, and metadata tags. ChromaDB automatically generates embeddings for each document using its default embedding function.
Write the retrieval function and test it
The retrieval function is the bridge between a user's profile and the knowledge base. It takes a natural-language query, finds the most relevant chunks, and returns them as a single string the LLM can use as context.
- Below the collection.add() block in the same cell, add the retrieval function and test prints:
def retrieve_context(query, n_results=5):
results = collection.query(
query_texts=[query],
n_results=n_results,
)
return "\n\n".join(results["documents"][0])
print("Knowledge base loaded:", collection.count(), "documents")
print("\nTest retrieval for 'protein for muscle gain':")
print(retrieve_context("protein requirements for muscle gain", n_results=3))
What does this code do?
retrieve_context() queries ChromaDB with a text string and returns the top N most semantically similar documents joined together. The query_texts parameter lets ChromaDB handle the embedding of your query automatically.
The print statements verify everything works: the document count confirms all 10 chunks loaded, and the test retrieval proves semantic search is returning relevant content for a fitness query.
Before you run this cell, what do you think the test query will return? Will it pull the protein guidelines, the muscle gain principles, or something else?
- Run the cell by clicking the play button (or pressing Shift+Enter).
You should see output starting with Knowledge base loaded: 10 documents followed by the top 3 chunks matching your query. The results should include the WHO/ISSN protein guidelines and muscle gain principles, since those are the most semantically relevant to "protein requirements for muscle gain."
Seeing an error or empty results?
- If you see NameError: name 'chromadb' is not defined, make sure Cell 2 (imports) has been run first. All imports must execute before this cell.
- If the model download seems stuck, the all-MiniLM-L6-v2 model is about 80MB. Give it a minute to download on the first run in Colab.
- If collection.count() returns 0, check that your fitness_knowledge list has all 10 dictionary entries with valid id, text, and metadata keys.
help me debug my ChromaDB knowledge base setup in Colab
✔️ Awesome, I've got everything!
Great. Confirm you have saved the notebook with all cells intact.
ⓧ I'd like to double check the full code
Your Cell 4 should contain these three sections in order:
- The fitness_knowledge list with 10 document dictionaries (WHO protein, USDA dietary, ACSM exercise, ISSN meal timing, hydration, weight loss, muscle gain, vegetarian, workout splits, budget nutrition).
- The ChromaDB initialization block: embedding_model, chroma_client, collection creation with cosine similarity, and collection.add() loading all documents.
- The retrieve_context() function followed by two print statements that verify the document count and test a retrieval query.
Scroll up through your cell and confirm each section is present, in order. When you run it, the output should display Knowledge base loaded: 10 documents and three relevant text paragraphs about protein and muscle gain.
Why ChromaDB instead of a keyword search?
A keyword search for "protein" would match every document that mentions the word, regardless of context. Semantic search using vector embeddings understands meaning. A query about "protein requirements for muscle gain" retrieves the ISSN sports nutrition document even though it never uses the exact phrase "protein requirements."
This is the core of RAG: finding the most relevant context so the LLM generates grounded answers instead of hallucinating generic advice.
Your knowledge base is loaded and searchable. Next up, you will wire this retrieval function into the LLM prompt to generate meal plans, workout schedules, and grocery lists that actually cite their sources.
Build the RAG-Powered Dashboard
In the previous step, you saw the LLM produce generic advice with no sources cited and no precise calorie targeting. The missing piece was domain-specific context from your knowledge base.
Now you combine everything into the full FitGenAI experience. The retrieval function pulls relevant guidelines from ChromaDB, the calculations provide exact macro targets, and the LLM generates plans that are both personalized AND grounded in real nutrition science.
In this step, get ready to:
- Build a RAG-augmented generation function that feeds retrieved context into the LLM prompt.
- Create a multi-tab Gradio dashboard with a profile form, meal plan, workout plan, and grocery list.
- Launch the app with a shareable public URL and test it on your phone.
Build the RAG-augmented generation function
Your generate_without_rag function sent raw user data to the LLM with no supporting context. The new function retrieves the most relevant knowledge base chunks first, then injects them directly into the prompt so the LLM can cite real guidelines.
- In your notebook, find Cell 5 where generate_without_rag is defined.
- Add the following function below it, starting with the function signature and prompt template:
def generate_with_rag(metrics, user_profile, food_preference, allergies, gym_days, fitness_goal):
# Build a search query from the user's goal and preferences
query = f"{fitness_goal} plan for {food_preference} diet, {gym_days} days training, allergies: {allergies}"
# Retrieve the top 5 most relevant knowledge base chunks
context = retrieve_context(query, n_results=5)
# Construct the augmented prompt with context and user data
prompt = f"""You are FitGenAI, an evidence-based fitness coach. Using ONLY the provided knowledge base context, generate personalized recommendations.
KNOWLEDGE BASE CONTEXT:
{context}
USER PROFILE:
{user_profile}
CALCULATED TARGETS:
- Daily Calories: {metrics['daily_calories']} kcal
- Protein: {metrics['protein_g']}g | Fat: {metrics['fat_g']}g | Carbs: {metrics['carb_g']}g
- Water: {metrics['water_l']}L per day
PREFERENCES:
- Diet: {food_preference}
- Allergies: {allergies}
- Training Days: {gym_days} days/week
- Goal: {fitness_goal}
INSTRUCTIONS:
1. Generate a 7-day meal plan that hits the calorie and macro targets. Respect dietary preferences and allergies.
2. Generate a weekly workout schedule based on available training days and fitness goal.
3. Generate a grocery list for the meal plan.
4. Cite which guideline informed each major recommendation (e.g., "Per ISSN guidelines...").
Format your response with clear sections: ## Meal Plan, ## Workout Plan, ## Grocery List"""
- Continuing directly below the prompt string (still inside the same function), add the API call:
# Send the augmented prompt to Groq with a system message enforcing citations
response = client.chat.completions.create(
messages=[
{"role": "system", "content": "You are FitGenAI, an evidence-based fitness coach. Always ground recommendations in the provided context. Cite sources."},
{"role": "user", "content": prompt}
],
model="llama-3.3-70b-versatile",
temperature=0.7,
max_tokens=4000,
)
return response.choices[0].message.content
What does this function do?
The function works in three stages. First, it builds a search query from the user's fitness goal, diet preference, and training schedule.
Second, it calls retrieve_context to pull the 5 most relevant chunks from your ChromaDB knowledge base. These chunks contain real guidelines from ISSN, ACSM, USDA, and other authorities.
Third, it packages everything into a prompt that explicitly tells the LLM to cite its sources and respect the calculated targets. The max_tokens=4000 gives the model enough room to generate a full 7-day plan.
- Run Cell 5 to define both functions.
- Test the new function by adding this quick test at the bottom of the cell and running it:
# Quick test of RAG generation
test_metrics = get_all_metrics(25, "Male", 175, 75, "Moderately active (3-5 days/week)", "Muscle Gain")
print(generate_with_rag(test_metrics, "25yo Male, 175cm, 75kg", "Non-Vegetarian", "", "4", "Muscle Gain")[:500])
Before you run this, do you think the output will look different from your Step 3 results?
You should see text that explicitly references guidelines like "Per ISSN guidelines" or "ACSM recommends" in the first 500 characters. That is the RAG context at work.
Seeing a rate limit error?
Groq's free tier has a limit of 1,000 requests per minute. If you ran many test calls in the previous step, wait 60 seconds and try again.
If the output looks generic with no citations, double-check that your retrieve_context function is working by running print(retrieve_context("protein for muscle gain", n_results=3)) in a separate cell.
Help me debug my generate_with_rag function
Create the Gradio dashboard handler functions
The generation function works, but the learner still needs a proper interface. Gradio lets you build full web UIs with pure Python, and it works natively in Colab with shareable links.
- Create a new Cell 6 below Cell 5.
- Add the profile processing function that calculates metrics and formats them as a markdown table:
# Store user data so other tabs can access it
user_metrics_store = {}
def process_profile(age, gender, height_cm, weight_kg, activity_level, fitness_goal,
target_duration, gym_days, food_preference, allergies, medical_conditions, budget):
# Calculate all fitness metrics from user inputs
metrics = get_all_metrics(age, gender, height_cm, weight_kg, activity_level, fitness_goal)
user_metrics_store["metrics"] = metrics
user_metrics_store["profile"] = f"{age}yo {gender}, {height_cm}cm, {weight_kg}kg, {activity_level}"
# Format metrics as a markdown table for display
metrics_display = f"""## Your Fitness Metrics
| Metric | Value |
|--------|-------|
| BMR (Basal Metabolic Rate) | {metrics['bmr']} kcal/day |
| TDEE (Total Daily Energy Expenditure) | {metrics['tdee']} kcal/day |
| Daily Calorie Target | {metrics['daily_calories']} kcal/day |
| Protein | {metrics['protein_g']}g/day |
| Fat | {metrics['fat_g']}g/day |
| Carbohydrates | {metrics['carb_g']}g/day |
| Water Intake | {metrics['water_l']}L/day |
"""
return metrics_display
What does this code do?
The user_metrics_store dictionary persists the user's profile data across tabs. Without it, each tab would lose track of who the user is.
The process_profile function calls your existing get_all_metrics calculator and formats the results into a clean markdown table that Gradio renders in the UI.
- Below process_profile in the same Cell 6, add the plan generation handler that orchestrates the full RAG pipeline:
def generate_plans(age, gender, height_cm, weight_kg, activity_level, fitness_goal,
target_duration, gym_days, food_preference, allergies, medical_conditions, budget):
# Calculate metrics and build a profile summary
metrics = get_all_metrics(age, gender, height_cm, weight_kg, activity_level, fitness_goal)
user_profile = f"{age}yo {gender}, {height_cm}cm, {weight_kg}kg, {activity_level}, goal: {fitness_goal}"
# Store profile data for the chatbot (Secret Mission)
user_metrics_store["metrics"] = metrics
user_metrics_store["profile"] = user_profile
user_metrics_store["food_preference"] = food_preference
user_metrics_store["allergies"] = allergies
# Call the RAG generation function
result = generate_with_rag(metrics, user_profile, food_preference, allergies, gym_days, fitness_goal)
# Parse the LLM response into separate sections by heading
sections = {"meal_plan": "", "workout_plan": "", "grocery_list": ""}
current_section = "meal_plan"
for line in result.split("\n"):
lower_line = line.lower()
if "## meal plan" in lower_line:
current_section = "meal_plan"
elif "## workout plan" in lower_line:
current_section = "workout_plan"
elif "## grocery list" in lower_line:
current_section = "grocery_list"
sections[current_section] += line + "\n"
return sections["meal_plan"], sections["workout_plan"], sections["grocery_list"]
Why parse the response into sections?
The prompt instructs the LLM to format its response with ## Meal Plan, ## Workout Plan, and ## Grocery List headings. The parser splits on those headings so each section can be displayed in its own Gradio tab.
This gives the user a clean, tabbed experience instead of one giant wall of text.
Build the Gradio interface and launch
With both handler functions ready, you can now build the visual interface. Gradio's Blocks API lets you create a multi-tab dashboard with form inputs, buttons, and output panels.
- Continue in Cell 6. Add the Gradio Blocks layout with the profile form and input fields:
with gr.Blocks(title="FitGenAI - AI Fitness Coach", theme=gr.themes.Soft()) as demo:
gr.Markdown("# FitGenAI - Your AI-Powered Fitness Coach\nFill in your profile to get personalized, evidence-based fitness recommendations.")
with gr.Tab("Profile & Metrics"):
with gr.Row():
with gr.Column():
age = gr.Number(label="Age", value=25, minimum=15, maximum=80)
gender = gr.Dropdown(label="Gender", choices=["Male", "Female"], value="Male")
height_cm = gr.Number(label="Height (cm)", value=175, minimum=100, maximum=250)
weight_kg = gr.Number(label="Weight (kg)", value=75, minimum=30, maximum=200)
activity_level = gr.Dropdown(
label="Activity Level",
choices=[
"Sedentary (little or no exercise)",
"Lightly active (1-3 days/week)",
"Moderately active (3-5 days/week)",
"Very active (6-7 days/week)",
"Extra active (athlete/physical job)",
],
value="Moderately active (3-5 days/week)"
)
- Continue directly below (still inside the with gr.Blocks() context). Add the second column of inputs, the calculate button, and output tabs:
with gr.Column():
fitness_goal = gr.Dropdown(
label="Fitness Goal",
choices=["Weight Loss", "Muscle Gain", "Maintenance"],
value="Muscle Gain"
)
target_duration = gr.Dropdown(label="Target Duration", choices=["4 weeks", "8 weeks", "12 weeks", "16 weeks"], value="12 weeks")
gym_days = gr.Dropdown(label="Preferred Gym Days/Week", choices=["2", "3", "4", "5", "6"], value="4")
food_preference = gr.Dropdown(label="Food Preference", choices=["Non-Vegetarian", "Vegetarian", "Vegan"], value="Non-Vegetarian")
allergies = gr.Textbox(label="Allergies (comma-separated)", placeholder="e.g., peanuts, dairy")
medical_conditions = gr.Textbox(label="Medical Conditions", placeholder="e.g., diabetes, hypertension")
budget = gr.Dropdown(label="Budget", choices=["Low", "Medium", "High"], value="Medium")
calc_btn = gr.Button("Calculate My Metrics", variant="primary")
metrics_output = gr.Markdown()
calc_btn.click(
fn=process_profile,
inputs=[age, gender, height_cm, weight_kg, activity_level, fitness_goal,
target_duration, gym_days, food_preference, allergies, medical_conditions, budget],
outputs=metrics_output
)
- Finally, add the output tabs, the generate button, and launch the app:
with gr.Tab("Meal Plan"):
meal_output = gr.Markdown(label="Your 7-Day Meal Plan")
with gr.Tab("Workout Plan"):
workout_output = gr.Markdown(label="Your Weekly Workout Plan")
with gr.Tab("Grocery List"):
grocery_output = gr.Markdown(label="Your Grocery List")
with gr.Tab("Profile & Metrics"):
generate_btn = gr.Button("Generate My Personalized Plans (RAG)", variant="secondary")
generate_btn.click(
fn=generate_plans,
inputs=[age, gender, height_cm, weight_kg, activity_level, fitness_goal,
target_duration, gym_days, food_preference, allergies, medical_conditions, budget],
outputs=[meal_output, workout_output, grocery_output]
)
demo.launch(share=True)
What does this UI code do?
- The Profile & Metrics tab holds both the input form and the two action buttons.
- The Calculate My Metrics button runs your Mifflin-St Jeor calculations instantly.
- The Generate My Personalized Plans (RAG) button triggers the full RAG pipeline and routes the parsed output to the Meal Plan, Workout Plan, and Grocery List tabs.
- The demo.launch(share=True) call creates a public URL you can open on any device.
- Remove the quick test lines you added at the bottom of Cell 5 earlier (the test_metrics and print lines).
- Run Cell 6.
Before you look at the output, what do you expect to see in the cell output when the app launches?
You should see a message like Running on public URL: https://xxxxxxxx.gradio.live printed in the output. Click that link to open your FitGenAI dashboard in a new browser tab.
- In the dashboard, leave the default values (25-year-old Male, 75kg, 175cm, Moderately active, Muscle Gain).
- Click Calculate My Metrics.
- Confirm you see a table showing BMR, TDEE, daily calories, protein, fat, carbs, and water intake values.
- Click Generate My Personalized Plans (RAG).
- Switch to the Meal Plan tab and read the output.
Compare this to the generic output you saw in Step 3. The meal plan now references specific calorie targets per meal, the workout plan cites exercise frequency guidelines, and the response explicitly mentions which guideline informed each recommendation.
- Copy the public URL from the cell output and open it on your phone to confirm the dashboard is accessible from another device.
App not launching or URL not appearing?
If you see a NameError, make sure you ran Cells 1 through 5 first. Cell 6 depends on functions and variables defined in the earlier cells.
If the URL never appears, check that you included share=True in the demo.launch() call. Without it, the app only runs locally inside the Colab VM.
Help me get my Gradio dashboard to launch in Colab
✔️ Awesome, I've got everything!
Great. Double-check you saved Cell 6 and the Gradio app is still running with the public URL active.
ⓧ I'd like to double check the full code
Your Cell 6 is long. Scroll up through your cell and confirm each section is present in this order:
- user_metrics_store = {} at the top.
- The process_profile function with the markdown table template.
- The generate_plans function with the section parser loop.
- The with gr.Blocks(...) block containing all tabs, buttons, and .click() wiring.
- demo.launch(share=True) as the final line.
For Cell 5, confirm both generate_without_rag and generate_with_rag are defined (without the quick test lines).
Your FitGenAI dashboard is live and accessible from any device. Anyone with the link can fill in their profile and get evidence-based, personalized fitness plans grounded in real nutrition science.
Secret mission
Add an AI Fitness Chatbot
Your dashboard generates plans, but what if you have follow-up questions? Add a conversational AI coach tab that answers questions like "Can I replace chicken with tofu?" using the same RAG pipeline and your stored profile.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely in Google Colab with Groq's free tier, so there are no ongoing costs.
Resources you used:
- Google Colab notebook (stored in your Google Drive).
- Groq API key (stored in Colab Secrets and in your Groq console).
- Gradio share link (temporary, expires automatically).
Keep everything running
No action needed. Choose this if you plan to continue experimenting with FitGenAI or want to keep the notebook as a reference.
- Your Colab notebook stays in Google Drive until you delete it.
- Your Groq API key remains active in Colab Secrets and ready for future notebooks.
- The Colab runtime will disconnect automatically after a period of inactivity, freeing up compute resources.
- The Gradio share link expires on its own after 72 hours.
Pause - I'll come back to this later
Shut down the running Colab session to free up resources, but keep the notebook and API key for later.
- In your Colab notebook, click Runtime in the top menu bar.
- Click Disconnect and delete runtime.
- This frees up RAM and GPU but keeps all your code cells intact in Google Drive.
- When you return, just click Runtime then Run all to pick up where you left off.
Delete - I don't want to use this again
Remove all project resources and start fresh.
- Delete the Colab notebook: go to drive.google.com, find your notebook in the Colab Notebooks folder, right-click it, and select Remove.
- Empty your trash: click Trash in the Google Drive sidebar, then click Empty Trash to permanently delete the file.
- Remove your Groq API key from Colab: click the key icon in the left sidebar to open Secrets, find GROQ_API_KEY, and click the delete icon next to it.
- Delete the key from Groq: go to console.groq.com/keys, locate your API key, and click the delete option to revoke it permanently.
Nice Work!
Nice Work!
You just built FitGenAI, a personalized AI fitness coach that calculates real nutritional targets and generates evidence-based meal plans, workout schedules, and grocery lists. You can open the shareable link on your phone and show anyone a working AI-powered fitness dashboard.
You've learned how to:
- Build a RAG pipeline with ChromaDB vector storage and Sentence Transformers embeddings that retrieves domain-specific knowledge to ground LLM outputs in real guidelines.
- Connect to Groq's free Llama 3.3 70B model and use prompt engineering to generate personalized fitness plans that cite their sources.
- Build a multi-tab Gradio dashboard with fitness calculations, generated plans, and a shareable public link accessible on any device.
- Secret Mission: Added a conversational AI fitness chatbot that answers follow-up questions using the same RAG pipeline and maintains multi-turn conversation history.
Ready to quiz yourself?