Skip to content

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:

  1. Self-host Tandoor (a recipe manager)
  2. Let an AI agent learn taste preferences through likes/dislikes
  3. Set up a cronjob to generate 2 meals every day

No more decision fatigue. The agent knows what I like.

Here’s the architecture:

Meal Planning 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:

docker-compose.yml setup
# Create docker-compose.yml
version: '3'
services:
tandoor:
image: vabene1111/recipes:latest
ports:
- "8080:8080"
environment:
- SECRET_KEY=your-secret-key
- DEBUG=0
volumes:
- ./tandoor_data:/opt/recipes/mediafiles

Then 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:

  1. Generate an API token in Tandoor settings
  2. Write a Python client to fetch and create recipes
tandoor_client.py
import requests
from dataclasses import dataclass
from typing import List
@dataclass
class 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
preference_learning.py
from dataclasses import dataclass, field
from typing import Set
import json
from pathlib import Path
@dataclass
class 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:

meal_selector.py
import random
from datetime import datetime
from 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 meals

The 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:

daily_meals.py
import sys
from pathlib import Path
# Configuration
TANDOOR_URL = "http://localhost:8080"
TANDOOR_TOKEN = "your-api-token-here"
PROFILE_PATH = Path.home() / ".meal_planner" / "taste_profile.json"
# Import our modules
from tandoor_client import TandoorClient, Recipe
from preference_learning import TasteProfile
from 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:

Crontab entry
# Edit crontab
crontab -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>&1

How to Give Feedback

The system only works if I tell it what I like. I added a simple CLI:

feedback.py
import sys
from pathlib import Path
from tandoor_client import TandoorClient
from 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:

Terminal window
# I liked today's lunch
python feedback.py 42 like
# I didn't enjoy the dinner
python feedback.py 43 dislike

What I Learned

After running this for two weeks:

  1. Decision fatigue is real - Not thinking about meals freed up surprising mental bandwidth
  2. The agent gets smarter - After 10-15 feedback cycles, suggestions got noticeably better
  3. Variety matters - The random factor prevents meal fatigue
  4. 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:

  1. No randomness - Same meals every day. Fixed by adding random noise to scores.
  2. Over-weighting likes - Agent kept suggesting variations of the same dish. Fixed by penalizing recently liked recipes.
  3. 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