My Dinner Planner Kept Suggesting Corned Beef in August. So I Taught It to Have Taste.
Part two: how I grounded an AI dinner planner in real recipes, and turned my family's palate into 384 numbers the system could actually learn.
A few weeks ago I wrote about Yossio Meals, the self-hosted dinner planner I built to kill the 5 p.m. "what's for dinner?" conversation. The thesis of that piece was a division of labor: AI drafts, humans decide, code executes. The AI proposes a plan as schema-validated JSON, deterministic Python does the math and the shopping list, and nothing hits the calendar without my approval. That system is still running. My family still uses it every day.
But living with a thing for a few weeks is the only real code review. Two flaws surfaced that no test suite would ever have caught, because they weren't bugs. They were the system doing exactly what I told it to.
The first: the AI was inventing recipes out of thin air. Ask a language model for "a Tuesday dinner" and it will happily conjure a plausible-sounding dish that exists nowhere, with quantities it guessed and a technique it half-remembers. The second, and more embarrassing: it had no memory of us. It would suggest corned beef and cabbage in the second week of August. Four times. It didn't know that dish is a March thing in our house, or that half the "creative" ideas it surfaced were oddball plates I would never actually cook.
The planner worked. It just didn't know anything about my kitchen. This is the story of fixing that, and it's the same lesson as last time, one level deeper.
AI drafts, humans decide, code executes. Part one killed the hallucinated math. Part two is about killing the hallucinated recipe and the generic palate underneath it.
The Problem With a Blank Page
An empty prompt is an invitation to make things up. If you ask a model to write a recipe, it writes fiction: confident and fluent, with no connection to anything you'd actually want to eat on a Wednesday. The fix wasn't a better prompt. It was to stop asking the model to author dinner at all.
I rebuilt the planning flow to be menu-first and grounded. Instead of one big "plan my week" call, the system now builds a skeleton first: seven empty slots that already respect the calendar and the household rules. Then it grounds each slot against a library of real recipes before the AI is allowed to write a single word of prose. The model's job shifts from author to curator: pick a dish that already exists, then enrich it.
That library is a supply chain, tried in order:
- A local corpus of real recipes, cached on the box so it works offline and costs nothing per lookup.
- Public recipe APIs, used sparingly for coverage the local set doesn't have.
- My own Mealie library, the "pantry" from part one, so my saved recipes get first-class treatment alongside everything else.
- The AI, and only the AI, as a last resort when the shelf is genuinely empty.
A word on where the recipes come from, because it matters and I want to be precise: the local corpus is built from an openly-licensed recipe dataset published for exactly this kind of use, cached locally as offline reference material. The API lookups run on public free tiers, within each provider's published guidelines, for the occasional dish my local set can't cover. Mealie is my own self-hosted book of recipes I've saved to cook. This is a personal project feeding my own family. No scraping, no stripping of terms, nothing that leans on a service in a way it wasn't built for. The whole point of the local corpus is to not hammer anyone's API.
Don't ask a model to invent dinner. Ask it to pick something real off the shelf, and only let it write when the shelf is empty.
The corned-beef-in-August problem got a second fix at the same layer: the coordinator that assembles the skeleton now knows what month it is. It's told the week it's planning for and instructed to weigh season and occasion. Corned beef is a March dish. Grilled things belong in July. The model already knows this; it just needed to be reminded that the calendar is a constraint, not decoration.
Teaching It the Season and the House
While I was in there, I fixed a small bug that had been quietly annoying me and merged two controls that never should have been separate.
The bug was a good one, in the way that only your own code can humble you. I have a slider for how much of a plan should come from my own saved recipes versus fresh ideas. Set it to zero, and I expected zero of my recipes: pure discovery mode. But it kept injecting exactly one. The cause was an off-by-one classic: the loop appended a recipe before checking whether it had hit its target, so a target of zero still let one slip through. Moving the check to the top of the loop fixed it. One line. It's always one line.
The controls were a different kind of clutter. I'd shipped a "max time per meal" dropdown and a separate "complexity" dropdown, and living with them made it obvious they were the same question asked twice. Nobody thinks "45 minutes AND moderate difficulty." They think "it's a weeknight, keep it easy." So I collapsed both into a single effort control: Quick, Weeknight, Involved, or Any, each mapping to a sensible time-and-difficulty pair under the hood. And I added an adventurousness lever, a dial from "give me the comfortable stuff" to "surprise me," so I can ask for a boring restorative week or a bold one without rewriting my whole request.
Taste as a Coordinate
Here's the real leap, and it's the part I'm proudest of.
Part one handled taste as prose: hard rules baked into the prompt. No fish except shrimp. No kale. No quinoa. That works for absolutes, but taste isn't a list of bans. It's a thousand soft preferences: this over that, this but rarely, this only in winter. You can't write that down as rules without going mad. So I stopped trying to describe taste in words and started treating it as geometry.
Every dish gets turned into a vector, a list of 384 numbers, by a local embedding model (BAAI/bge-small-en-v1.5, running on the same box, no API, no per-call cost). Dishes that are conceptually similar land near each other in that 384-dimensional space; a pot roast and a braised short rib are close, a Caprese salad is far away. That's the substrate the whole taste system is built on.
On top of that, two centers of gravity. Every dish I thumbs-up nudges a "liked" centroid, the weighted average position of everything my family enjoys. Every thumbs-down pulls a "disliked" centroid the other way. To score a brand-new dish the planner has never seen, it just measures which center of gravity the dish sits closer to. No rules. No prompt engineering. Just distance.
Taste isn't a sentence you write. It's a center of gravity you move.
The beautiful thing about this is that it generalizes. I don't have to rate every dish in the world. I rate a few hundred, the centroids settle, and a dish I've never laid eyes on gets a sensible score the moment it appears, because it's near things I've already judged.
The Thumbs
Geometry needs data, and data needs an interface that doesn't feel like data entry. So I built a Taste screen: a grid of dishes pulled from the corpus, each with a thumbs-up and a thumbs-down, that I can burn through in a couple of minutes on my phone. Tap, tap, tap. Rated cards fade out; new ones slide in. There's a running counter of how many I've rated and a "new set" button when I want fresh faces.
Then it got interesting, because a bare thumbs-up throws away why. So under each rating there are quick reason chips: comfort food, too heavy, kid-friendly, too unusual, not my style. Here's the twist that makes them more than tags: each chip is itself embedded as a concept and folded into the centroids. "Comfort food" isn't a label on one dish; it's a direction in the taste space. Thumbs-up three things and tap "comfort food" on each, and the system learns to pull the whole neighborhood of comforting dishes toward my liked centroid, including ones I've never rated. The reasons transfer.
Two more refinements came straight out of using it:
Frequency is a slider, not a switch. Corned beef isn't a dish I dislike. It's a dish I love and want twice a year. Early on the system couldn't tell the difference between "no" and "not often," which is a huge distinction in a real kitchen. So every liked dish gets a frequency slider on a linear scale, from "every couple of weeks" on one end to "once or twice a year" on the other. Now a special-occasion favorite can score high on taste and still stay out of the weeknight rotation, exactly like it does in real life.
Dedup by dish family, not by string. Remember the four corned beefs? The corpus had twenty-eight near-identical variants of it. My first instinct was to dedupe by vector similarity, but it turned out those variants sat only about 0.72 cosine apart: close, but under any threshold I'd have trusted to not also collapse genuinely different dishes. So I switched to matching on shared significant keywords. Two dishes that share enough meaningful words are the same family, and only one representative surfaces. The wall of corned beef finally came down.
The cards also tell me what I already know about a dish before I judge it: a "made it before" badge if it's in my cooking history, a "new to you" badge if it isn't, plus calories and a snippet of any review. Enough context to rate honestly without clicking into the full recipe.
Not-a-Dinner Is Still Data
The first version of the Taste screen had an "exclude" button for things that aren't dinners: croutons, stock, breadcrumbs, a cocktail. Then I caught myself about to delete a perfectly good entry for chicken stock and realized I was throwing away knowledge. Stock isn't a bad dinner. It's a part of cooking.
So "exclude" became reclassify. Instead of deleting a mis-filed dish, I re-shelve it: this is a lunch, an appetizer, a side, a component, a drink. It leaves the dinner feed but stays in the system, correctly labeled, ready to be pulled in wherever it actually belongs. Data survives. Nothing gets thrown away just because it showed up in the wrong place.
Closing the Loop After the Stove
Rating recipes I might cook is only half the signal. The other half is how the meal actually went. So the loop now closes at the stove: after I cook something, the recipe view has a quiet "how was it?" panel, with thumbs, what I liked or didn't, a "made it again" toggle, and a box for notes to my future self ("double the sauce," "kids picked out the peppers").
And because part one's whole ethos is that Mealie is the pantry, the durable home for recipe knowledge, those notes don't just live in the planner. When I save a reviewed meal to Mealie, my family notes ride along into the recipe itself. The lesson lands where I'll actually see it next time, in the book, not buried in an app I might rebuild someday.
The Mirror
Once you've turned taste into numbers, you can hold up a mirror. The Taste screen has a profile page with two halves. The first is deterministic: straight counts pulled from my ratings, the proteins I gravitate toward and the ones I pass over, how often I want things, what wins me over, what puts me off, and the new dishes the centroids predict I'd most likely love.
The second half is the same data handed to the AI with one instruction: describe this family's food style, personified. What does this kitchen read like? It's a genuinely strange and useful thing to see your own preferences narrated back at you by a model that only knows you as a cloud of vectors. It gets us more right than I'm comfortable with.
Engineering War Stories
Building your own kitchen stack remains a special kind of humbling, and this round had its share.
I destroyed my own data. Again. Last time it was a fuzzy-search cleanup script that wiped a recipe I cared about. This time, mid-development, a test-cleanup run did a blanket delete against the ratings table and took real ratings with it, the ones I'd actually sat and entered. The daily backups I built after the first disaster meant it wasn't fatal, and I restored the rows by hand. But the lesson finally stuck for good: test data and real data must never share a table you're willing to run DELETE against. Every test now uses synthetic dish names with far-future dates, walled off from anything real. Nothing motivates a discipline like learning the same lesson twice.
The silent no-op. For an afternoon, my thumbs-up and thumbs-down did absolutely nothing to the centroids, and I couldn't see why. No error, no crash, just zero learning. The cause was two tables keyed differently: the ratings table keyed each dish by its normalized name, while the embeddings table keyed by a hash of that name. My update code looked up the embedding using the normalized name instead of the hash, found nothing, and cheerfully adjusted a weight on a record that didn't exist. The worst bugs don't throw. They just quietly do nothing while you congratulate yourself.
The threshold that was almost right. The corned-beef dedup taught me that "almost the right similarity threshold" is worse than a clearly wrong one. At 0.72 the duplicates hid just under my cutoff, so the feature looked like it worked, until the fourth corned beef showed up. Sometimes the fix isn't tuning the number. It's changing the question, from "how close are these vectors?" to "do these dishes share the words that matter?"
By the Numbers
- 1,000+ — Real recipes the planner now grounds against before the AI writes anything
- 384 — Dimensions in every dish's taste vector
- 138 — Ratings collected so far, enough for a coherent taste fingerprint
- 83 — Dishes re-shelved out of the dinner feed as sides, components, or drinks
What Part Three Might Hold
- [ ] Make the AI taste profile editable and actionable, a paragraph I can tweak that steers the planner directly, not just a mirror.
- [ ] Smarter "made it before" detection using dish-family similarity, so a variant of something I've cooked counts as familiar.
- [ ] A taste-aware "surprise me" that picks bold dishes from near my liked centroid instead of at random.
- [ ] A tag filter to find meals by the qualities I've been marking: "show me the comforting, kid-friendly ones."
- [ ] Tighter backup cadence around the ratings table specifically, because apparently I need it.
The Blueprint, One Level Deeper
- Ground before you generate. Don't let a model invent what already exists. Give it a real library to curate from, and only fall back to generation when the shelf is empty. Curation hallucinates far less than authorship.
- Turn preferences into geometry. Hard rules handle absolutes; everything softer belongs in a vector space where "similar" is a distance, not a sentence. A center of gravity you can move beats a rulebook you have to maintain.
- Make the signal cheap to give. Learning dies if teaching it feels like a chore. Thumbs and chips on a phone, a few minutes at a time, beat any form with a text box.
- "Wrong" is still data. Don't delete a mis-filed thing: reclassify it. The knowledge of what something is is worth keeping even when it's not what you were looking for.
- Learn the same lesson once. If a mistake can happen twice, it will. Wall your tests off from anything real before it does.
Common Questions on Teaching a Planner Taste
Isn't this just a recommendation engine?
In spirit, yes, but a personal, single-household one running entirely on my own hardware. There's no cold-start problem to solve for millions of users and no data leaving the box. The embedding model, the corpus, and the centroids all live locally, which is what makes it cheap enough to run for a family of a few.
Why local embeddings instead of an API?
Cost and privacy. Embedding thousands of dishes and re-scoring them on every plan would rack up API calls fast, and I'd rather my family's eating habits not be a line item in someone's telemetry. A small open embedding model runs comfortably on the same box that hosts everything else, for free, forever.
Does grounding make the AI less creative?
It makes it creative in the right place. The model still gets to synthesize and write the enrichment, substituting and riffing on top of a real dish instead of inventing the dish itself. I'd rather have an inventive take on a recipe that exists than a beautifully described one that doesn't.
The Journey Isn't Over
Part one was about removing the decision. Part two is about removing the sameness: getting a system to stop suggesting the generic, seasonally-confused, oddball plates that a model reaches for when it doesn't know you, and to start reflecting how a specific family actually eats. It's not finished. Taste drifts, kids grow, seasons turn, and the centroids will keep moving. But the planner has stopped guessing and started learning, and that's a different kind of tool to live with.
If part one killed the question, part two is teaching the thing that answers it to actually know us. That feels like the whole point.
Stephen M. Yoss, CPA, CPA & Software Developer
Stephen is a certified public accountant, the CEO and partner of Devmatics, LLC, a continuing education instructor for financial professionals, a professional speaker at live events, and a licensed pyrotechnician. While his interests and skills are varied, they all share a common thread: his love for and skill in finding technology-based solutions.
Whether it's teaching in the classroom, consulting clients in a boardroom, or shooting a fireworks display, Steve brings passion, hard work, value, and above all else, technological expertise to each of his clients. With an open, honest approach, he creates a unique strategy for each client specifically designed to benefit their needs and simplify their operations in order to create efficiency and maximize their financial potential.