Spaces:
Sleeping
Sleeping
import os | |
from dotenv import load_dotenv | |
import gradio as gr | |
from string import Template | |
from typing import List | |
from pydantic import BaseModel, Field | |
# ----- Load API Keys ----- | |
load_dotenv() | |
google_api_key = os.getenv('GOOGLE_API_KEY') | |
tavily_api_key = os.getenv('TAVILY_API_KEY') | |
# ----- Pydantic Models ----- | |
class NutritionInfo(BaseModel): | |
calories: str = Field(..., description="Total calories in the meal") | |
protein: str = Field(..., description="Protein content in grams") | |
carbs: str = Field(..., description="Carbohydrates content in grams") | |
fat: str = Field(..., description="Fat content in grams") | |
class BudgetMeal(BaseModel): | |
meal_name: str = Field(..., description="Name of the meal") | |
ingredients: List[str] = Field(..., description="List of ingredients with quantities") | |
cooking_steps: List[str] = Field(..., description="Step-by-step cooking instructions") | |
nutrition_info: NutritionInfo = Field(..., description="Nutritional breakdown of the meal") | |
reason_for_selection: str = Field(..., description="Explanation for why this meal was chosen") | |
class BudgetBasedMeal(BaseModel): | |
low_budget: BudgetMeal = Field(..., description="Low-budget version of the meal") | |
medium_budget: BudgetMeal = Field(..., description="Medium-budget version of the meal") | |
high_budget: BudgetMeal = Field(..., description="High-budget version of the meal") | |
class ThreeMealPlan(BaseModel): | |
meal_type: str | |
breakfast: BudgetBasedMeal | |
lunch: BudgetBasedMeal | |
dinner: BudgetBasedMeal | |
class FourMealPlan(BaseModel): | |
meal_type: str | |
breakfast: BudgetBasedMeal | |
lunch: BudgetBasedMeal | |
snack: BudgetBasedMeal | |
dinner: BudgetBasedMeal | |
class IntermittentFastingPlan(BaseModel): | |
meal_type: str | |
lunch: BudgetBasedMeal | |
dinner: BudgetBasedMeal | |
# ----- Prompt Template ----- | |
prompt_template = Template( | |
""" | |
You are a top nutritionist specializing in personalized meal planning. Based on the user's profile, create a personalized one-day meal plan that STRICTLY adheres to their dietary preferences and COMPLETELY EXCLUDES any ingredients they are allergic to. The user follows the "$meal_plan_type" pattern. | |
IMPORTANT DIETARY GUIDELINES: | |
1. STRICTLY follow the user's dietary preference: $dietary_preference | |
2. ABSOLUTELY AVOID any ingredients listed in allergies/restrictions: $allergies | |
- Double-check each ingredient to ensure NO allergens are included | |
- If an ingredient could contain hidden allergens, choose a safe alternative | |
Meal Plan Type Guide: | |
- "3 meals/day" β breakfast, lunch, dinner | |
- "4 meals/day" β breakfast, lunch, snack, dinner | |
- "Intermittent fasting (2 meals)" β lunch, dinner | |
Each meal should have **3 flexible options** while maintaining dietary requirements: | |
- low budget (affordable while meeting dietary needs) | |
- medium budget (balanced options within dietary restrictions) | |
- high budget (premium ingredients following dietary preferences) | |
Each option must include: | |
- Meal name (clearly indicating it follows dietary preferences) | |
- Ingredients (all safe and compliant with dietary restrictions) | |
- Cooking steps | |
- Basic nutrition info (calories, protein, carbs, fat) | |
- Short reason for choosing this meal based on the user's chosen package and dietary needs | |
User Profile: | |
- Age group: $age_group | |
- Height: $height inches | |
- Weight: $weight lbs | |
- Gender: $gender | |
- Dietary preference: $dietary_preference (STRICT ADHERENCE REQUIRED) | |
- Allergies or restrictions: $allergies (MUST BE COMPLETELY AVOIDED) | |
- Goal/package: $package | |
Output format must be clean and JSON-like, without extra keys. Just the meals as per plan type with 3 budget-based options each. Every meal MUST comply with dietary preferences and exclude allergens. | |
""" | |
) | |
# ----- Tavily Tool ----- | |
from langchain_tavily import TavilySearch | |
tavily_tool = TavilySearch( | |
max_results=20, | |
topic="general", | |
include_answer=True, | |
include_raw_content=True, | |
search_depth="advanced", | |
tavily_api_key=tavily_api_key, | |
include_domains=[ | |
"https://www.nutritionvalue.org/", | |
"https://www.walmart.com/search?q=", | |
"https://www.healthline.com/nutrition", | |
"https://www.healthline.com/nutrition/meal-kits", | |
"https://www.healthline.com/nutrition/meal-kits/diets", | |
"https://www.healthline.com/nutrition/special-diets", | |
"https://www.healthline.com/nutrition/healthy-eating", | |
"https://www.healthline.com/nutrition/food-freedom", | |
"https://www.healthline.com/nutrition/feel-good-food", | |
"https://www.healthline.com/nutrition/products", | |
"https://www.healthline.com/nutrition/vitamins-supplements", | |
"https://www.healthline.com/nutrition/sustain", | |
], | |
) | |
# ----- LLM + Agents ----- | |
from langchain_google_genai import ChatGoogleGenerativeAI | |
llm = ChatGoogleGenerativeAI(model="gemini-2.5-pro-preview-03-25", google_api_key=google_api_key) | |
from langgraph.prebuilt import create_react_agent | |
agent_3_meals = create_react_agent( | |
llm, | |
tools=[tavily_tool], | |
response_format=ThreeMealPlan | |
) | |
agent_4_meals = create_react_agent( | |
llm, | |
tools=[tavily_tool], | |
response_format=FourMealPlan | |
) | |
agent_intermittent = create_react_agent( | |
llm, | |
tools=[tavily_tool], | |
response_format=IntermittentFastingPlan | |
) | |
# ----- Render Functions ----- | |
def render_meal(meal: BudgetMeal, budget_label: str) -> str: | |
ingredients_str = "\n- ".join(meal.ingredients) | |
steps_str = "\n1. ".join(meal.cooking_steps) | |
return ( | |
f"### {budget_label} Option\n\n" | |
f"**π½οΈ Meal Name**: {meal.meal_name}\n\n" | |
f"**π Ingredients**:\n- {ingredients_str}\n\n" | |
f"**π¨βπ³ Cooking Steps**:\n1. {steps_str}\n\n" | |
f"**π Nutrition Info**:\n" | |
f"- Calories: {meal.nutrition_info.calories}\n" | |
f"- Protein: {meal.nutrition_info.protein}\n" | |
f"- Carbs: {meal.nutrition_info.carbs}\n" | |
f"- Fat: {meal.nutrition_info.fat}\n\n" | |
f"**π‘ Why this meal?**\n{meal.reason_for_selection}\n" | |
) | |
def render_budget_meal(meal_obj: BudgetBasedMeal, meal_type: str) -> str: | |
return ( | |
f"## π± {meal_type.title()}\n\n" | |
f"{render_meal(meal_obj.low_budget, 'Low Budget')}\n" | |
f"{render_meal(meal_obj.medium_budget, 'Medium Budget')}\n" | |
f"{render_meal(meal_obj.high_budget, 'High Budget')}\n" | |
) | |
def format_three_meal_plan(plan: ThreeMealPlan) -> str: | |
return ( | |
f"# π§Ύ Meal Plan: {plan.meal_type}\n\n" | |
f"{render_budget_meal(plan.breakfast, 'Breakfast')}\n" | |
f"{render_budget_meal(plan.lunch, 'Lunch')}\n" | |
f"{render_budget_meal(plan.dinner, 'Dinner')}\n" | |
) | |
def format_four_meal_plan(plan: FourMealPlan) -> str: | |
return ( | |
f"# π§Ύ Meal Plan: {plan.meal_type}\n\n" | |
f"{render_budget_meal(plan.breakfast, 'Breakfast')}\n" | |
f"{render_budget_meal(plan.lunch, 'Lunch')}\n" | |
f"{render_budget_meal(plan.snack, 'Snack')}\n" | |
f"{render_budget_meal(plan.dinner, 'Dinner')}\n" | |
) | |
def format_if_plan(plan: IntermittentFastingPlan) -> str: | |
return ( | |
f"# π§Ύ Meal Plan: {plan.meal_type}\n\n" | |
f"{render_budget_meal(plan.lunch, 'Lunch')}\n" | |
f"{render_budget_meal(plan.dinner, 'Dinner')}\n" | |
) | |
# ----- Generate Meal Plan Function ----- | |
def generate_meal_plan(age_group, feet, inches, weight, gender, meal_plan_type, dietary_preference, allergies, package): | |
# Compute total height in inches: | |
total_height = int(feet) * 12 + int(inches) | |
user_input = { | |
"age_group": age_group, | |
"height": str(total_height), | |
"weight": str(weight), | |
"gender": gender, | |
"meal_plan_type": meal_plan_type, | |
"dietary_preference": dietary_preference, | |
"allergies": allergies, | |
"package": package | |
} | |
filled_prompt = prompt_template.substitute(**user_input) | |
inputs = {"messages": [("user", filled_prompt)]} | |
if meal_plan_type.startswith("3 meals/day"): | |
result = agent_3_meals.invoke(inputs)["structured_response"] | |
formatted = format_three_meal_plan(result) | |
elif meal_plan_type.startswith("4 meals/day"): | |
result = agent_4_meals.invoke(inputs)["structured_response"] | |
formatted = format_four_meal_plan(result) | |
else: # Intermittent Fasting | |
result = agent_intermittent.invoke(inputs)["structured_response"] | |
formatted = format_if_plan(result) | |
return formatted | |
# ----- Gradio UI ----- | |
demo = gr.Blocks() | |
with demo: | |
gr.Markdown("## π½οΈ Personalized Meal Plan Generator") | |
with gr.Row(): | |
age_group = gr.Dropdown(choices=["18-24", "25-30", "31-40", "41-50", "51+"], label="Age Group") | |
gender = gr.Dropdown(choices=["male", "female", "other"], label="Gender") | |
with gr.Row(): | |
feet = gr.Number(label="Height (feet)") | |
inches = gr.Number(label="Height (inches)") | |
weight = gr.Number(label="Weight (lbs)") | |
meal_plan_type = gr.Radio( | |
choices=[ | |
"3 meals/day (Breakfast, Lunch, Dinner)", | |
"4 meals/day (Breakfast, Lunch, Snack, Dinner)", | |
"Intermittent fasting (2 meals)" | |
], | |
label="Meal Plan Type" | |
) | |
dietary_preference = gr.Dropdown( | |
choices=["Keto", "Vegan", "Vegetarian", "Low-Carb", "High-Protein", "Balanced"], | |
label="Dietary Preference" | |
) | |
allergies = gr.Textbox(label="Allergies or Restrictions (e.g., Gluten, Dairy, Nuts or 'None')") | |
package = gr.Dropdown( | |
choices=["Fitness and Mobility", "Focus Flow", "No More Insomnia"], | |
label="Goal/Package" | |
) | |
with gr.Row(): | |
# Add a status indicator | |
status_indicator = gr.Markdown("Status: Ready") | |
generate_btn = gr.Button("Generate Meal Plan") | |
output_display = gr.Markdown() | |
# Add the loading indicator logic | |
def generate_with_loading(age_group, feet, inches, weight, gender, meal_plan_type, dietary_preference, allergies, package): | |
# Return a loading message for the status indicator | |
return "Status: Generating your meal plan... This may take a moment! β³" | |
def finalize_generation(age_group, feet, inches, weight, gender, meal_plan_type, dietary_preference, allergies, package): | |
# Generate the meal plan | |
result = generate_meal_plan(age_group, feet, inches, weight, gender, meal_plan_type, dietary_preference, allergies, package) | |
# Update the status indicator | |
return "Status: Ready", result | |
# Connect the button click to both functions in sequence | |
generate_btn.click( | |
fn=generate_with_loading, | |
inputs=[age_group, feet, inches, weight, gender, meal_plan_type, dietary_preference, allergies, package], | |
outputs=status_indicator, | |
).then( | |
fn=finalize_generation, | |
inputs=[age_group, feet, inches, weight, gender, meal_plan_type, dietary_preference, allergies, package], | |
outputs=[status_indicator, output_display], | |
) | |
demo.launch() |