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()