feat: Implement AI Recipe & Meal Planning Agent with recipe search, nutrition analysis, cost estimation, and meal planning features

This commit is contained in:
Shubhamsaboo
2025-08-14 02:03:44 -05:00
parent 0b7d8389cd
commit 92cd20fc47
3 changed files with 544 additions and 0 deletions

View File

@@ -0,0 +1,164 @@
# 🍽️ AI Recipe & Meal Planning Agent
An intelligent meal planning agent built with Agno that helps you discover recipes, analyze nutrition, estimate costs, and create weekly meal plans based on your ingredients and dietary preferences.
## Features
🔍 **Recipe Discovery**
- Find recipes based on available ingredients
- Support for dietary restrictions (vegetarian, vegan, keto, paleo, etc.)
- Ingredient substitution suggestions
- Detailed cooking instructions and timing
📊 **Nutrition Analysis**
- Comprehensive nutritional breakdown per serving
- User-friendly health assessments
- Calorie, protein, carb, and fat tracking
- Sodium and fiber content analysis
💰 **Cost Estimation**
- Grocery cost estimation for ingredients
- Budget-friendly meal suggestions
- Cost per serving calculations
📅 **Weekly Meal Planning**
- Balanced meal plans for any household size
- Dietary preference accommodation
- Shopping list optimization
- Budget-conscious planning
🧠 **Session-Based Conversations**
- Remembers context during your current browser session
- Preferences are not persisted after restart (no long-term storage)
### How to get Started?
1. Clone the GitHub repository
```bash
git clone https://github.com/Shubhamsaboo/awesome-llm-apps.git
cd advanced_ai_agents/single_agent_apps/ai_recipe_meal_planning_agent
```
2. Install the required dependencies:
```bash
pip install -r requirements.txt
```
3. Get your OpenAI API Key
- Sign up for an [OpenAI account](https://platform.openai.com/) and obtain your API key.
4. Get your Spoonacular API Key
- Sign up for a [Spoonacular account](https://spoonacular.com/food-api) and obtain your API key (free tier ~50 requests/day).
5. Create a `.env` file in this folder
```bash
# Required
OPENAI_API_KEY=your_openai_api_key_here
# Optional but recommended for full recipe & nutrition functionality
SPOONACULAR_API_KEY=your_spoonacular_api_key_here
```
6. Run the Streamlit App
```bash
streamlit run ai_recipe_meal_planning_agent.py
```
7. Open your browser at `http://localhost:8501`
## Example Interactions
**Recipe Discovery:**
- "I have chicken, broccoli, and rice. What can I make?"
- "Find me vegan recipes using lentils"
- "Show me quick 30-minute dinner ideas"
**Nutrition Analysis:**
- "What's the nutritional content of this recipe?"
- "Is this meal high in protein?"
- "How many calories per serving?"
**Meal Planning:**
- "Create a week's worth of vegetarian meals for 2 people"
- "I need a low-sodium meal plan"
- "Plan budget-friendly meals for a family of 4"
**Cost Estimation:**
- "How much will these ingredients cost?"
- "What's the most budget-friendly option?"
- "Estimate weekly grocery costs for this meal plan"
## Application Architecture
### Built with Agno Framework
- **Agent**: OpenAI GPT-5 mini powered meal planning agent
- **Memory**: Conversation memory for personalized recommendations
- **Tools**: Custom tools for recipe search and analysis + DuckDuckGo web search
- **Interface**: Streamlit web application
### Custom Tools
1. `search_recipes(ingredients, diet_type=None)` - Recipe discovery via Spoonacular API with detailed instructions
2. `analyze_nutrition(recipe_name)` - Detailed nutritional analysis via Spoonacular
3. `estimate_costs(ingredients, servings=4)` - Budget planning and cost estimation
4. `create_meal_plan(dietary_preference="balanced", people=2, days=7, budget="moderate")` - Comprehensive weekly meal planning with shopping list
5. `DuckDuckGoTools` - Web search for additional context
### Key Technologies
- **Agno**: AI agent framework
- **Streamlit**: Web interface and user interaction
- **Spoonacular API**: Recipe and nutrition data
- **OpenAI GPT-5 mini**: Natural language understanding and generation
## Customization
### Adding New Dietary Preferences
Modify the `search_recipes` tool to include additional diet types supported by Spoonacular API.
### Extending Cost Database
Update the `ingredient_costs` dictionary in `estimate_grocery_costs()` with local pricing.
### Custom Meal Categories
Edit the `meal_categories` in `create_weekly_meal_plan()` to match your preferences.
## Troubleshooting
**API Key Issues:**
- Ensure your `.env` file is in the correct directory
- Verify API keys are valid and have sufficient credits
- Check API key format (no extra spaces or quotes)
- Note: Without `SPOONACULAR_API_KEY`, recipe search and nutrition tools will return an error; other features will still load.
**Recipe Search Not Working:**
- Verify Spoonacular API key is set correctly
- Check your API usage limits (150 requests/day for free tier)
- Try simpler ingredient searches
**Memory Issues:**
- The agent uses conversation memory to remember preferences
- Clear browser cache if experiencing persistent issues
- Restart the application to reset conversation history
## Contributing
Feel free to contribute by:
- Adding new recipe sources or APIs
- Improving nutrition analysis algorithms
- Enhancing cost estimation accuracy
- Adding new meal planning features
## License
This project is open source. Please check the main repository for license details.
## Support
For issues and questions:
- Check the troubleshooting section above
- Review the Agno documentation
- Open an issue in the main repository

View File

@@ -0,0 +1,375 @@
import asyncio
import os
import streamlit as st
import random
from textwrap import dedent
from typing import Dict, List, Optional
from agno.agent import Agent
from agno.models.openai import OpenAIChat
from agno.tools import tool
import requests
from dotenv import load_dotenv
from agno.tools.duckduckgo import DuckDuckGoTools
load_dotenv()
SPOONACULAR_API_KEY = os.getenv("SPOONACULAR_API_KEY")
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
@tool
def search_recipes(ingredients: str, diet_type: Optional[str] = None) -> Dict:
"""Search for detailed recipes with cooking instructions."""
if not SPOONACULAR_API_KEY:
return {"error": "Spoonacular API key not found"}
url = "https://api.spoonacular.com/recipes/findByIngredients"
params = {
"apiKey": SPOONACULAR_API_KEY,
"ingredients": ingredients,
"number": 5,
"ranking": 2,
"ignorePantry": True
}
if diet_type:
params["diet"] = diet_type
try:
response = requests.get(url, params=params, timeout=15)
response.raise_for_status()
recipes = response.json()
detailed_recipes = []
for recipe in recipes[:3]:
detail_url = f"https://api.spoonacular.com/recipes/{recipe['id']}/information"
detail_response = requests.get(detail_url, params={"apiKey": SPOONACULAR_API_KEY}, timeout=10)
if detail_response.status_code == 200:
detail_data = detail_response.json()
detailed_recipes.append({
"id": recipe['id'],
"title": recipe['title'],
"ready_in_minutes": detail_data.get('readyInMinutes', 'N/A'),
"servings": detail_data.get('servings', 'N/A'),
"health_score": detail_data.get('healthScore', 0),
"used_ingredients": [i['name'] for i in recipe['usedIngredients']],
"missing_ingredients": [i['name'] for i in recipe['missedIngredients']],
"instructions": detail_data.get('instructions', 'Instructions not available')
})
return {
"recipes": detailed_recipes,
"total_found": len(recipes)
}
except:
return {"error": "Recipe search failed"}
@tool
def analyze_nutrition(recipe_name: str) -> Dict:
"""Get nutrition analysis for a recipe by searching for it."""
if not SPOONACULAR_API_KEY:
return {"error": "API key not found"}
# First search for the recipe
search_url = "https://api.spoonacular.com/recipes/complexSearch"
search_params = {
"apiKey": SPOONACULAR_API_KEY,
"query": recipe_name,
"number": 1,
"addRecipeInformation": True,
"addRecipeNutrition": True
}
try:
search_response = requests.get(search_url, params=search_params, timeout=15)
search_response.raise_for_status()
search_data = search_response.json()
if not search_data.get('results'):
return {"error": f"No recipe found for '{recipe_name}'"}
recipe = search_data['results'][0]
if 'nutrition' not in recipe:
return {"error": "No nutrition data available for this recipe"}
nutrients = {n['name']: n['amount'] for n in recipe['nutrition']['nutrients']}
calories = round(nutrients.get('Calories', 0))
protein = round(nutrients.get('Protein', 0), 1)
carbs = round(nutrients.get('Carbohydrates', 0), 1)
fat = round(nutrients.get('Fat', 0), 1)
fiber = round(nutrients.get('Fiber', 0), 1)
sodium = round(nutrients.get('Sodium', 0), 1)
# Health insights
health_insights = []
if protein > 25:
health_insights.append("✅ High protein - great for muscle building")
if fiber > 5:
health_insights.append("✅ High fiber - supports digestive health")
if sodium < 600:
health_insights.append("✅ Low sodium - heart-friendly")
if calories < 400:
health_insights.append("✅ Low calorie - good for weight management")
return {
"recipe_title": recipe.get('title', 'Recipe'),
"servings": recipe.get('servings', 1),
"ready_in_minutes": recipe.get('readyInMinutes', 'N/A'),
"health_score": recipe.get('healthScore', 0),
"calories": calories,
"protein": protein,
"carbs": carbs,
"fat": fat,
"fiber": fiber,
"sodium": sodium,
"health_insights": health_insights
}
except:
return {"error": "Nutrition analysis failed"}
@tool
def estimate_costs(ingredients: List[str], servings: int = 4) -> Dict:
"""Detailed cost estimation with budget tips."""
prices = {
"chicken breast": 6.99, "ground beef": 5.99, "salmon": 12.99,
"rice": 2.99, "pasta": 1.99, "broccoli": 2.99, "tomatoes": 3.99,
"cheese": 5.99, "onion": 1.49, "garlic": 2.99, "olive oil": 7.99
}
cost_breakdown = []
total_cost = 0
for ingredient in ingredients:
ingredient_lower = ingredient.lower().strip()
cost = 3.99 # default
for key, price in prices.items():
if key in ingredient_lower or any(word in ingredient_lower for word in key.split()):
cost = price
break
adjusted_cost = (cost * servings) / 4
total_cost += adjusted_cost
cost_breakdown.append({
"name": ingredient.title(),
"cost": round(adjusted_cost, 2)
})
# Budget tips
budget_tips = []
if total_cost > 30:
budget_tips.append("💡 Consider buying in bulk for better prices")
if total_cost > 40:
budget_tips.append("💡 Look for seasonal alternatives to reduce costs")
budget_tips.append("💡 Shop at local markets for fresher, cheaper produce")
return {
"total_cost": round(total_cost, 2),
"cost_per_serving": round(total_cost / servings, 2),
"servings": servings,
"breakdown": cost_breakdown,
"budget_tips": budget_tips
}
@tool
def create_meal_plan(dietary_preference: str = "balanced", people: int = 2, days: int = 7, budget: str = "moderate") -> Dict:
"""Create comprehensive weekly meal plan with nutrition and shopping list."""
meals = {
"breakfast": [
{"name": "Overnight Oats with Berries", "calories": 320, "protein": 12, "cost": 2.50},
{"name": "Veggie Scramble with Toast", "calories": 280, "protein": 18, "cost": 3.20},
{"name": "Greek Yogurt Parfait", "calories": 250, "protein": 15, "cost": 2.80}
],
"lunch": [
{"name": "Quinoa Buddha Bowl", "calories": 420, "protein": 16, "cost": 4.50},
{"name": "Chicken Caesar Wrap", "calories": 380, "protein": 25, "cost": 5.20},
{"name": "Lentil Vegetable Soup", "calories": 340, "protein": 18, "cost": 3.80}
],
"dinner": [
{"name": "Grilled Salmon with Vegetables", "calories": 520, "protein": 35, "cost": 8.90},
{"name": "Chicken Stir Fry with Brown Rice", "calories": 480, "protein": 32, "cost": 6.50},
{"name": "Vegetable Curry with Quinoa", "calories": 450, "protein": 15, "cost": 5.20}
]
}
budget_multipliers = {"low": 0.7, "moderate": 1.0, "high": 1.3}
multiplier = budget_multipliers.get(budget.lower(), 1.0)
weekly_plan = {}
shopping_list = set()
total_weekly_cost = 0
total_weekly_calories = 0
total_weekly_protein = 0
day_names = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
for day in day_names[:days]:
daily_meals = {}
daily_calories = 0
daily_protein = 0
daily_cost = 0
for meal_type in ["breakfast", "lunch", "dinner"]:
selected_meal = random.choice(meals[meal_type])
daily_meals[meal_type] = {
"name": selected_meal["name"],
"calories": selected_meal["calories"],
"protein": selected_meal["protein"]
}
meal_cost = selected_meal["cost"] * people * multiplier
daily_calories += selected_meal["calories"]
daily_protein += selected_meal["protein"]
daily_cost += meal_cost
# Add to shopping list
if "chicken" in selected_meal["name"].lower():
shopping_list.add("Chicken breast")
if "salmon" in selected_meal["name"].lower():
shopping_list.add("Salmon fillets")
if "vegetable" in selected_meal["name"].lower():
shopping_list.update(["Mixed vegetables", "Onions", "Garlic"])
if "quinoa" in selected_meal["name"].lower():
shopping_list.add("Quinoa")
if "oats" in selected_meal["name"].lower():
shopping_list.add("Rolled oats")
weekly_plan[day] = daily_meals
total_weekly_cost += daily_cost
total_weekly_calories += daily_calories
total_weekly_protein += daily_protein
# Generate insights
avg_daily_calories = round(total_weekly_calories / days)
avg_daily_protein = round(total_weekly_protein / days, 1)
insights = []
if avg_daily_calories < 1800:
insights.append("⚠️ Consider adding healthy snacks to meet calorie needs")
elif avg_daily_calories > 2200:
insights.append("💡 Calorie-dense meals - great for active lifestyles")
if avg_daily_protein > 80:
insights.append("✅ Excellent protein intake for muscle maintenance")
elif avg_daily_protein < 60:
insights.append("💡 Consider adding more protein sources")
return {
"meal_plan": weekly_plan,
"total_weekly_cost": round(total_weekly_cost, 2),
"cost_per_person_per_day": round(total_weekly_cost / (people * days), 2),
"avg_daily_calories": avg_daily_calories,
"avg_daily_protein": avg_daily_protein,
"dietary_preference": dietary_preference,
"serves": people,
"days": days,
"shopping_list": sorted(list(shopping_list)),
"insights": insights
}
async def create_agent():
agent = Agent(
name="MealPlanningExpert",
model=OpenAIChat(id="gpt-5-mini"),
tools=[search_recipes, analyze_nutrition, estimate_costs, create_meal_plan, DuckDuckGoTools()],
instructions=dedent("""\
You are an expert meal planning assistant. Provide detailed, helpful responses:
🔍 **Recipe Searches**: Include cooking time, health scores, ingredient lists, and instructions
📊 **Nutrition Analysis**: Provide health insights, nutritional breakdowns, and dietary advice
💰 **Cost Estimation**: Include budget tips and cost per serving breakdowns
📅 **Meal Planning**: Create detailed weekly plans with nutritional balance and shopping lists
**Always**:
- Use clear headings and bullet points
- Include practical cooking tips
- Consider dietary restrictions and budgets
- Provide actionable next steps
- Be encouraging and supportive
"""),
markdown=True,
show_tool_calls=True
)
return agent
def main():
st.set_page_config(page_title="AI Meal Planning Agent", page_icon="🍽️", layout="wide")
st.title("🍽️ AI Meal Planning Agent")
st.markdown("*Your intelligent companion for recipes, nutrition, and meal planning*")
if not OPENAI_API_KEY:
st.error("Please add OPENAI_API_KEY to your .env file")
st.stop()
# Initialize agent
if "agent" not in st.session_state:
with st.spinner("Initializing agent..."):
try:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
st.session_state.agent = loop.run_until_complete(create_agent())
except Exception as e:
st.error(f"Failed to initialize agent: {e}")
st.stop()
# Initialize messages
if "messages" not in st.session_state:
st.session_state.messages = [{
"role": "assistant",
"content": """👋 **Welcome! I'm your AI Meal Planning Expert.**
I can help you with:
- 🔍 **Recipe Discovery** - Find recipes based on your ingredients
- 📊 **Nutrition Analysis** - Get detailed nutritional insights
- 💰 **Cost Estimation** - Smart budget planning with money-saving tips
- 📅 **Meal Planning** - Complete weekly meal plans with shopping lists
**Try asking:**
- "Find healthy chicken recipes for dinner"
- "What's the nutrition info for chicken teriyaki?"
- "Create a vegetarian meal plan for 2 people for one week"
- "Estimate costs for pasta, tomatoes, cheese, and basil for 4 servings"
What would you like to explore? 🍽️"""
}]
# Chat interface
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
# Chat input
if user_input := st.chat_input("Ask about recipes, nutrition, meal planning, or costs..."):
st.session_state.messages.append({"role": "user", "content": user_input})
with st.chat_message("user"):
st.markdown(user_input)
with st.chat_message("assistant"):
with st.spinner("Thinking..."):
try:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
response = loop.run_until_complete(
st.session_state.agent.arun(user_input)
)
st.markdown(response.content)
st.session_state.messages.append({
"role": "assistant",
"content": response.content
})
except Exception as e:
error_msg = f"Error: {str(e)}"
st.error(error_msg)
st.session_state.messages.append({
"role": "assistant",
"content": error_msg
})
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,5 @@
streamlit
agno
python-dotenv
requests
duckduckgo-search