This is a submission for the Hacktoberfest Weekend Challenge: Build for a Friend
What I Built
SafePlate is a tiny command-line meal planner for a friend with food allergies (in my example profile: peanut and shrimp, loves spicy food, rice and tofu, student budget). It asks a locally running open-weight model to plan a week of dinners, and then a plain-Python allergen checker vets every single meal before it reaches the plan.
The problem it solves is a very human one. Anyone who cooks for a friend with allergies knows the anxiety: "is there shrimp paste in that sambal?" Language models are great at creativity and bad at guarantees, so SafePlate splits the job: the model proposes, deterministic code disposes. If the model slips (for example suggests "sambal terasi", which hides shrimp paste), the meal is rejected, fed back to the model as "do not repeat", and retried. If no safe meal is found after three tries, the tool refuses to guess and prints NO SAFE MEAL FOUND - please plan this one by hand.
An honest note up front: I haven't handed this to a real person yet, so I won't invent a reaction. The friend profile in the repo is an example you edit (friend.json), and the tool is meant to be adapted to whoever you cook for.
Demo
There's no hosted demo, since the whole point is that it runs on your own machine. Run it like this:
ollama pull gemma3
python3 safeplate.py friend.json
I tested the planner and safety logic with a scripted stand-in for the model (see the test below), including the "model slips and mentions shrimp paste" case. I did not benchmark Gemma's real output quality, so treat meal quality as untested.
Code
Everything is stdlib-only Python, one file plus a test.
safeplate.py:
#!/usr/bin/env python3
"""SafePlate: a local, offline-friendly meal planner for a friend with food allergies.
The open-weight model (Gemma via Ollama) proposes meals. A plain-Python
allergen checker then vets every ingredient, so safety never depends on the model.
Stdlib only.
"""
import json, sys, urllib.request
OLLAMA = "http://localhost:11434/api/generate"
MODEL = "gemma3" # any Ollama model works
# Allergen -> words that must never appear in an ingredient list.
ALLERGENS = {
"peanut": ["peanut", "groundnut", "satay"],
"tree nut": ["almond", "cashew", "walnut", "pecan", "pistachio", "hazelnut", "marzipan"],
"shrimp": ["shrimp", "prawn", "terasi", "belacan", "krill"],
"egg": ["egg", "mayonnaise", "meringue"],
"milk": ["milk", "butter", "cheese", "cream", "yogurt", "ghee", "whey"],
"gluten": ["wheat", "flour", "bread", "pasta", "noodle", "soy sauce", "barley"],
}
def banned_words(profile):
words = []
for a in profile["allergies"]:
words += ALLERGENS.get(a, [a])
return [w.lower() for w in words]
def ask_model(prompt, model=MODEL):
body = json.dumps({"model": model, "prompt": prompt, "stream": False,
"format": "json"}).encode()
req = urllib.request.Request(OLLAMA, body, {"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=300) as r:
return json.loads(json.loads(r.read())["response"])
def build_prompt(profile, day, rejected):
avoid = f"\nThese were rejected, do not repeat: {rejected}" if rejected else ""
return (
f"Plan one day of dinner for {profile['name']}. Allergies (STRICT, avoid even traces): "
f"{', '.join(profile['allergies'])}. Likes: {', '.join(profile['likes'])}. "
f"Budget: {profile['budget']}. Day: {day}.{avoid}\n"
'Reply ONLY as JSON: {"meal": str, "ingredients": [str], "steps": [str]}'
)
def violations(meal, profile):
text = " ".join(meal.get("ingredients", [])).lower() + " " + meal.get("meal", "").lower()
return sorted({w for w in banned_words(profile) if w in text})
def plan_week(profile, ask=ask_model, days=("Mon","Tue","Wed","Thu","Fri","Sat","Sun"), retries=3):
plan, rejected = [], []
for day in days:
for _ in range(retries):
meal = ask(build_prompt(profile, day, rejected))
bad = violations(meal, profile)
if not bad:
plan.append((day, meal)); break
rejected.append(f"{meal.get('meal')} (contains {bad})")
else:
plan.append((day, {"meal": "NO SAFE MEAL FOUND - please plan this one by hand",
"ingredients": [], "steps": []}))
return plan
def main():
path = sys.argv[1] if len(sys.argv) > 1 else "friend.json"
profile = json.load(open(path))
for day, m in plan_week(profile):
print(f"\n{day}: {m['meal']}\n buy: {', '.join(m['ingredients'])}")
for i, s in enumerate(m["steps"], 1): print(f" {i}. {s}")
if __name__ == "__main__":
main()
friend.json:
{"name": "my roommate", "allergies": ["peanut", "shrimp"], "likes": ["spicy food", "rice", "tofu"], "budget": "student"}
test_safeplate.py (passes, using a fake model):
import json
import safeplate as s
P = json.loads(open("friend.json").read())
calls = []
def fake(prompt):
calls.append(prompt)
if len(calls) == 1: # model slips up: shrimp paste hides in a sambal
return {"meal": "Sambal rice", "ingredients": ["rice", "sambal terasi"], "steps": ["cook"]}
return {"meal": "Tofu stir-fry", "ingredients": ["tofu", "rice", "chili"], "steps": ["fry"]}
plan = s.plan_week(P, ask=fake, days=("Mon",))
assert plan[0][1]["meal"] == "Tofu stir-fry"
assert "Sambal rice" in calls[1] # rejected meal fed back to model
assert s.violations({"meal":"Pad thai","ingredients":["peanuts"]}, P) == ["peanut"]
always_bad = lambda p: {"meal":"x","ingredients":["prawn"],"steps":[]}
assert "NO SAFE MEAL" in s.plan_week(P, ask=always_bad, days=("Mon",))[0][1]["meal"]
print("all tests passed")
How I Built It
-
Open-weight model: Gemma (Google's open-weight model) served locally by Ollama. SafePlate talks to Ollama's local HTTP API with
format: "json"so the output is machine-checkable. - Propose / verify loop: the model only suggests. A deterministic ingredient blocklist (with regional aliases like terasi and belacan for shrimp paste) decides what is allowed. Rejections are fed back into the next prompt, so the model learns what to avoid within the same run.
- Fail closed: after three failed attempts the tool admits defeat instead of serving a risky meal.
- Zero dependencies, so it runs on a cheap laptop.
Known limits: a blocklist is not medical advice and can miss hidden or cross-contaminated ingredients. Always read labels and check with the person you cook for.
Why Does Open Innovation Matter?
Health information is about as private as data gets, and an allergy profile is exactly what I would not want to ship to a server I don't control. With an open-weight model running through local inference, the friend's profile never leaves the laptop, it works with no internet, and it costs nothing per meal plan. I can also swap models by changing one string (MODEL = "gemma3"), which matters because smaller local models make more mistakes, and the verifier lets me use them anyway. With a closed API, I would be trusting both the vendor's uptime and its handling of sensitive data.
My Agent Session
Not included.
Prize Categories
Best Use of Gemma: Gemma, served locally via Ollama, is the planner at the core of the project.
Disclosure: I built this with help from an AI assistant (Claude) and reviewed the code and tests myself.
Top comments (2)
What I find interesting here is that the model isn't being treated as the source of truth.
For example, an LLM could look at a user's profile and generate a meal containing shrimp because it misunderstood the user's allergy, ignored part of the context, or was manipulated by some unexpected input. The important part is that the application doesn't need to trust the model's reasoning to catch that.
You can have the model generate the meal, then run the result through a deterministic constraint layer:
LLM output โ ingredient extraction โ allergy/constraint validation โ accept or rejectThat pattern feels much more useful than trying to make the prompt perfect. The model handles the fuzzy part, while the application owns the rules that must not be violated. And I think that's where AI features start becoming real software architecture rather than just adding an LLM to an existing workflow.
Thanks, Sina! That's exactly the idea: the model handles the fuzzy part, and the rules that must never break live in plain code. One honest limit: my checker is a keyword blocklist, so it only catches what's on the list (it would miss a hidden ingredient it doesn't know about). Your "ingredient extraction โ validation" step is the natural next upgrade, e.g. asking the model to list every sub-ingredient first, then validating that list. Haven't built that yet, but it's the first thing I'd add.