mirror of
https://github.com/Shubhamsaboo/awesome-llm-apps.git
synced 2026-03-11 17:48:31 -05:00
feat: Implement AI Recipe & Meal Planning Agent with recipe search, nutrition analysis, cost estimation, and meal planning features
This commit is contained in:
@@ -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
|
||||
@@ -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()
|
||||
@@ -0,0 +1,5 @@
|
||||
streamlit
|
||||
agno
|
||||
python-dotenv
|
||||
requests
|
||||
duckduckgo-search
|
||||
Reference in New Issue
Block a user