How I Built an AI Agent That Plans My Meals Every Day
Problem
Every day at 5 PM, I faced the same question: “What should I cook for dinner?”
This sounds trivial. But after a full workday, my brain was fried. I’d stare at the fridge, scroll through recipe apps, and eventually order takeout. My grocery shopping was random. Food went bad. I ate the same three meals on repeat.
A Reddit user described the exact same problem:
“I had a problem where I didn’t know what to cook each day. So I made my agent create a list of foods and ingredients. Then I could like/dislike items so the agent knows what I like.”
That post got 13 upvotes. The solution worked. I decided to build my own version.
The Solution: AI + Tandoor
The Reddit user’s approach was simple:
- Self-host Tandoor (a recipe manager)
- Let an AI agent learn taste preferences through likes/dislikes
- Set up a cronjob to generate 2 meals every day
No more decision fatigue. The agent knows what I like.
Here’s the architecture:
+------------------+ +------------------+| User Input | | Recipe DB || (Like/Dislike) | | (Tandoor) |+--------+---------+ +--------+---------+ | | v v+------------------------------------------------------------------+| Preference Learning Layer || - Track liked/disliked ingredients || - Score recipes based on preferences |+------------------------------------------------------------------+ | v+------------------------------------------------------------------+| Automation Layer || - Cronjob: runs at 7 AM daily || - Selects 2 recipes matching preferences || - Pushes to Tandoor meal plan |+------------------------------------------------------------------+Step 1: Set Up Tandoor
Tandoor is a self-hosted recipe manager. I ran it with Docker:
# Create docker-compose.ymlversion: '3'services: tandoor: image: vabene1111/recipes:latest ports: - "8080:8080" environment: - SECRET_KEY=your-secret-key - DEBUG=0 volumes: - ./tandoor_data:/opt/recipes/mediafilesThen I imported my favorite recipes. Tandoor has a web interface for adding recipes manually or importing from URLs.
Step 2: Connect to Tandoor API
Tandoor exposes a REST API. I needed to:
- Generate an API token in Tandoor settings
- Write a Python client to fetch and create recipes
import requestsfrom dataclasses import dataclassfrom typing import List
@dataclassclass Recipe: id: int name: str ingredients: List[str] tags: List[str]
class TandoorClient: def __init__(self, base_url: str, api_token: str): self.base_url = base_url.rstrip('/') self.headers = {"Authorization": f"Token {api_token}"}
def get_recipes(self, limit: int = 100) -> List[Recipe]: """Fetch recipes from Tandoor""" response = requests.get( f"{self.base_url}/api/recipe/", headers=self.headers, params={"limit": limit} ) response.raise_for_status()
recipes = [] for item in response.json().get('results', []): # Extract ingredients from steps ingredients = [] for step in item.get('steps', []): for ing in step.get('ingredients', []): ingredients.append(ing['food']['name'])
recipes.append(Recipe( id=item['id'], name=item['name'], ingredients=ingredients, tags=[tag['name'] for tag in item.get('keywords', [])] )) return recipes
def add_to_meal_plan(self, recipe_id: int, date: str, meal_type: str = "dinner"): """Add recipe to meal plan for a specific date""" response = requests.post( f"{self.base_url}/api/meal-plan/", headers=self.headers, json={ "recipe": recipe_id, "date": date, "meal_type": meal_type } ) response.raise_for_status() return response.json()This gave me access to all my recipes programmatically.
Step 3: Build the Preference Learning System
The key insight from the Reddit post: the agent learns what I like over time.
I built a simple scoring system:
- When I like a recipe, all its ingredients and tags get positive points
- When I dislike a recipe, those ingredients/tags get negative points
- Recipes with higher scores are selected more often
from dataclasses import dataclass, fieldfrom typing import Setimport jsonfrom pathlib import Path
@dataclassclass TasteProfile: liked_ingredients: Set[str] = field(default_factory=set) disliked_ingredients: Set[str] = field(default_factory=set) liked_tags: Set[str] = field(default_factory=set) disliked_tags: Set[str] = field(default_factory=set) liked_recipes: Set[int] = field(default_factory=set) disliked_recipes: Set[int] = field(default_factory=set)
def score_recipe(self, recipe) -> float: """Score a recipe based on learned preferences""" score = 0.0
# Ingredients scoring for ing in recipe.ingredients: ing_lower = ing.lower() if ing_lower in self.liked_ingredients: score += 1.0 if ing_lower in self.disliked_ingredients: score -= 2.0 # Dislikes penalized more
# Tags scoring for tag in recipe.tags: tag_lower = tag.lower() if tag_lower in self.liked_tags: score += 0.5 if tag_lower in self.disliked_tags: score -= 1.0
# Avoid repeating recently liked recipes if recipe.id in self.liked_recipes: score -= 5.0
# Strongly avoid disliked recipes if recipe.id in self.disliked_recipes: score -= 10.0
return score
def record_feedback(self, recipe, liked: bool): """Record like/dislike and learn from it""" if liked: self.liked_recipes.add(recipe.id) for ing in recipe.ingredients: self.liked_ingredients.add(ing.lower()) for tag in recipe.tags: self.liked_tags.add(tag.lower()) else: self.disliked_recipes.add(recipe.id) for ing in recipe.ingredients: self.disliked_ingredients.add(ing.lower()) for tag in recipe.tags: self.disliked_tags.add(tag.lower())
def save(self, filepath: Path): """Persist preferences to disk""" data = { 'liked_ingredients': list(self.liked_ingredients), 'disliked_ingredients': list(self.disliked_ingredients), 'liked_tags': list(self.liked_tags), 'disliked_tags': list(self.disliked_tags), 'liked_recipes': list(self.liked_recipes), 'disliked_recipes': list(self.disliked_recipes) } filepath.parent.mkdir(parents=True, exist_ok=True) with open(filepath, 'w') as f: json.dump(data, f)
@classmethod def load(cls, filepath: Path) -> 'TasteProfile': """Load saved preferences""" if not filepath.exists(): return cls() with open(filepath, 'r') as f: data = json.load(f) return cls( liked_ingredients=set(data.get('liked_ingredients', [])), disliked_ingredients=set(data.get('disliked_ingredients', [])), liked_tags=set(data.get('liked_tags', [])), disliked_tags=set(data.get('disliked_tags', [])), liked_recipes=set(data.get('liked_recipes', [])), disliked_recipes=set(data.get('disliked_recipes', [])) )The scoring logic is intentional:
- Dislikes count double because I really don’t want to see those again
- Recently liked recipes get penalized to add variety
- Tags have less weight than ingredients
Step 4: Create the Meal Selector
Now I needed logic to pick recipes:
import randomfrom datetime import datetimefrom typing import List, Tuple
class MealSelector: def __init__(self, tandoor, profile): self.tandoor = tandoor self.profile = profile self.recipes = []
def refresh_recipes(self): """Reload recipes from Tandoor""" self.recipes = self.tandoor.get_recipes()
def select_meals(self, count: int = 2) -> List[Tuple]: """Select meals based on preferences with some randomness""" if not self.recipes: self.refresh_recipes()
# Score all recipes scored = [ (recipe, self.profile.score_recipe(recipe)) for recipe in self.recipes ]
# Filter out very low scores viable = [(r, s) for r, s in scored if s >= -5.0]
# Add randomness for variety (otherwise same recipes every day) viable.sort( key=lambda x: x[1] + random.uniform(-1, 1), reverse=True )
return viable[:count]
def generate_daily_meals(self) -> List: """Generate and save meals for today""" selected = self.select_meals(count=2) today = datetime.now().strftime('%Y-%m-%d')
meals = [] for i, (recipe, score) in enumerate(selected): meal_type = "lunch" if i == 0 else "dinner" self.tandoor.add_to_meal_plan(recipe.id, today, meal_type) meals.append(recipe)
return mealsThe random factor is important. Without it, the agent would pick the exact same meals every day.
Step 5: Set Up the Cronjob
The final piece: automate it. I created a script that runs daily:
import sysfrom pathlib import Path
# ConfigurationTANDOOR_URL = "http://localhost:8080"TANDOOR_TOKEN = "your-api-token-here"PROFILE_PATH = Path.home() / ".meal_planner" / "taste_profile.json"
# Import our modulesfrom tandoor_client import TandoorClient, Recipefrom preference_learning import TasteProfilefrom meal_selector import MealSelector
def main(): # Initialize tandoor = TandoorClient(TANDOOR_URL, TANDOOR_TOKEN) profile = TasteProfile.load(PROFILE_PATH) selector = MealSelector(tandoor, profile)
# Generate today's meals meals = selector.generate_daily_meals()
print(f"Generated {len(meals)} meals for today:") for meal in meals: print(f" - {meal.name}")
# Save profile profile.save(PROFILE_PATH)
if __name__ == "__main__": main()Then added it to crontab:
# Edit crontabcrontab -e
# Add this line (runs at 7 AM every day)0 7 * * * /usr/bin/python3 /path/to/daily_meals.py >> /var/log/meal_planner.log 2>&1How to Give Feedback
The system only works if I tell it what I like. I added a simple CLI:
import sysfrom pathlib import Pathfrom tandoor_client import TandoorClientfrom preference_learning import TasteProfile
def record_feedback(recipe_id: int, liked: bool): tandoor = TandoorClient("http://localhost:8080", "your-token") profile = TasteProfile.load(Path.home() / ".meal_planner" / "taste_profile.json")
# Find the recipe recipes = tandoor.get_recipes() recipe = next((r for r in recipes if r.id == recipe_id), None)
if recipe: profile.record_feedback(recipe, liked) profile.save(Path.home() / ".meal_planner" / "taste_profile.json") print(f"Recorded {'like' if liked else 'dislike'} for {recipe.name}")
if __name__ == "__main__": recipe_id = int(sys.argv[1]) liked = sys.argv[2].lower() == "like" record_feedback(recipe_id, liked)Usage:
# I liked today's lunchpython feedback.py 42 like
# I didn't enjoy the dinnerpython feedback.py 43 dislikeWhat I Learned
After running this for two weeks:
- Decision fatigue is real - Not thinking about meals freed up surprising mental bandwidth
- The agent gets smarter - After 10-15 feedback cycles, suggestions got noticeably better
- Variety matters - The random factor prevents meal fatigue
- Simple is enough - I didn’t need nutritional tracking or budget optimization
The Reddit user was right: “Now I don’t have to think about what to cook, and the agent knows what food I like.”
Common Pitfalls
When I first built this, I made these mistakes:
- No randomness - Same meals every day. Fixed by adding random noise to scores.
- Over-weighting likes - Agent kept suggesting variations of the same dish. Fixed by penalizing recently liked recipes.
- Forgetting to save profile - Preferences lost between runs. Fixed by persisting to JSON.
Summary
In this post, I showed how to build an AI meal planning agent with Python and Tandoor. The key components are:
- Tandoor for recipe storage and meal planning
- Preference learning that tracks likes/dislikes
- Cronjob automation for daily meal generation
The result: no more “what should I cook” decision fatigue. The agent knows my tastes and suggests meals I’ll actually enjoy.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments