An intelligent meal planning and fridge inventory management system structured as an official Hermes plugin. It helps users decide what to cook for dinner and what to buy at the grocery store by analyzing their current fridge contents, recipe catalog, and cooking history.
An AI assistant invokes the nineteen tool handlers registered via __init__.py:register(ctx) to deliver personalized dinner suggestions, generate optimized shopping lists, manage fridge inventory, manage the recipe catalog, track cooked meals, and interactively build ingredient lists via the Dynamic Ingredient Interface (DII) — all with zero external dependencies.
Traditional meal-planning apps fail in two ways. Some turn the user into a data-entry clerk, demanding constant manual input until the user abandons the system. Others hand so much control to an AI that behavior becomes unpredictable — suggestions change meaning between sessions, state drifts quietly, and users lose the trust that makes the tool useful.
meal-manager resolves this by separating concerns cleanly. The LLM acts as a semantic translator at the boundary: it interprets natural-language intent ("we had carbonara tonight", "add lasagna to my recipes") and maps it onto a typed, validated tool call. The plugin's core is a deterministic state machine — scoring, inventory updates, recipe storage, DII session transitions, and persistence are all pure, testable Python code with explicit constants and no model round-trips.
The result is a system that offers the user the freedom of conversation while guaranteeing the consistency of code. Ambiguity is resolved once, at the edge. Every decision past that edge is reproducible, auditable, and cheap.
The LLM is a Translator, Not a Judge. The model interprets user intent and maps it onto a tool schema. It does not rank meals, decide whether an ingredient is essential, or track session state — those belong to deterministic Python modules. This line stays fixed when the underlying model changes, the prompt drifts, or the user rephrases the same request two different ways.
Ambiguity Stops at the Schema. Free-text input is welcome in conversation; past the tool boundary, every argument is typed, normalized, and explicit. Schemas refuse fuzzy values — the LLM must commit to a concrete dish_name, a concrete action enum, a concrete is_essential boolean. The cost of interpretation is paid once, at parse time, and never re-paid by downstream logic. The database stays clean by construction, not by convention.
Reproducibility as User Trust. Given identical fridge contents, recipe catalog, and cooking history, the plugin always produces identical suggestions in identical order. The 60/40 match/recency blend, the 80/20 essential/optional weighting, the 2-day cooldown, and the 14-day recency cap are explicit constants in source — not emergent model output. Users can predict the system because the system predicts itself; every state transition can be replayed from the JSON files under data/.
Tokens Are a Cost, Not a Feature. Work the code can do does not belong in the prompt. Ranking, session state, ingredient normalization, and persistence run in microseconds without a model round-trip. The result is a plugin that is cheap to run, fast to respond, testable without mocking an LLM, and structurally incapable of hallucinating itself into an inconsistent state.
- Smart Meal Suggestions — Ranks every dish in the catalog using a weighted scoring algorithm that combines ingredient availability (60%) with cooking recency (40%). Dishes cooked fewer than 2 days ago are automatically excluded.
- One-Ingredient Shopping List — Identifies single ingredients that, once purchased, unlock entirely new dishes. Prioritized by the projected score of the unlocked meal.
- Fridge Inventory Management — Add or remove ingredients as you shop or cook. Ingredient and dish names are normalized to lowercase for consistent matching.
- Cooking History Tracking — Logs cooked meals with ISO dates. History keys are normalized to lowercase on load, so comparisons are case-insensitive.
- Auto-Cleanup on Cook — When a meal is registered as cooked, its essential ingredients are automatically removed from the fridge inventory.
- Essential vs. Optional Ingredients — Recipes distinguish between must-have ingredients (required to cook) and nice-to-have ingredients (boost the suggestion score but are not blocking).
- Dynamic Ingredient Interface (DII) — Interactive, stateful ingredient selection via plain text conversation. A "probability funnel" reveals ranked ingredient suggestions one at a time. The agent interprets free-text user responses (e.g. "yes", "skip", "add X") to drive add/skip/remove/manual-add controls. Removing an essential ingredient triggers a recalculation signal so the agent can re-evaluate suggestions.
| Layer | Technology |
|---|---|
| Language | Python 3.12+ |
| Dependencies | None (standard library only) |
| Data Storage | Local JSON files (data/) |
| Architecture | Official Hermes plugin (plugin.yaml + register(ctx)) |
| Data Modeling | Python dataclasses |
- Python 3.12 or newer installed on your system.
- No package manager or virtual environment is required — the project has zero external dependencies.
-
Clone the repository:
git clone https://github.com/sergiparpal/meal-manager.git cd meal-manager -
Verify your Python version:
python3 --version # Should be 3.12+ -
Run the tests to verify everything works:
python3 test_unit.py python3 test_integration.py
No build step, dependency installation, or configuration is needed. Data files under data/ are created lazily by the tools when first needed.
Once the plugin is installed in your Hermes agent, you never invoke any tool yourself. You talk to the agent in natural language and it translates your intent into the right tool calls. There are no commands to memorize — say what you mean and the agent will handle the bookkeeping.
Example phrases and what the agent will do behind the scenes:
Deciding what to cook
- "What should I cook tonight?" — ranks your cookable dishes and proposes the best one.
- "We had carbonara." — records the meal, applies the 2-day cooldown, and removes its essential ingredients from the fridge automatically.
Shopping
- "I'm heading to the grocery store, what should I buy?" — lists single ingredients that, once purchased, unlock the best dishes.
- "I bought onions, peppers, and chicken." — updates the fridge and proposes new meal ideas with what you have now.
- "We ran out of milk." — removes it from the fridge inventory.
Managing the fridge
- "What do I have in the fridge?" — returns the current inventory.
- "Empty the fridge, I'm going on vacation." — clears all fridge contents.
Teaching new recipes
- "I usually make potato omelette." — the agent infers ingredients from culinary knowledge, shows them for your confirmation, then saves the recipe.
- "Add lasagna, cannelloni, and paella to my recipes." — adds several dishes in a single pass.
- "Carbonara doesn't carry cream, fix the recipe." — replaces the ingredient list of an existing dish.
- "Delete the chicken curry recipe." — removes it from the catalog.
Correcting mistakes
- "I didn't actually cook that yesterday." — removes the meal from history so it can be suggested again without waiting for the cooldown.
Interactive ingredient picking (DII)
When you add a new dish without listing its ingredients, the agent starts a step-by-step session. It proposes one ingredient at a time and you reply in plain text:
Agent: I suggest parmesan cheese (optional). Add it, skip it, or something else?
You: skip — add pepper instead
Agent: Added pepper. Next suggestion: garlic (optional)…
Reply naturally — "yes", "skip", "remove X", "also add Y", or "done" when finished. There's no menu to navigate.
First time? If your catalog is empty or has fewer than five dishes, the agent will proactively offer to help populate it — just tell it what you usually cook and it will infer ingredients, confirm them with you, and save everything in one batch.
The plugin is loaded by a Hermes agent via the register(ctx) entry point in __init__.py. It registers nineteen tools:
| Tool | Purpose |
|---|---|
get_meal_suggestions |
Returns a ranked list of dishes you can cook right now |
get_quick_shopping_list |
Returns single-ingredient purchases that unlock new dishes |
update_fridge_inventory |
Adds or removes ingredients from the fridge |
register_cooked_meal |
Logs a dish as cooked today and removes its essential ingredients |
delete_history_entry |
Undo for register_cooked_meal — removes a dish from history |
list_fridge |
Returns the current fridge contents |
add_dish |
Adds a new recipe to the catalog |
add_dishes_batch |
Adds multiple recipes in a single call |
delete_dish |
Removes a recipe from the catalog |
edit_dish |
Replaces the ingredients of an existing dish |
clear_fridge |
Empties the fridge completely |
init_ingredient_session |
Start a DII session with ranked ingredient suggestions |
dii_add_suggested |
Accept the current ingredient suggestion and reveal the next |
dii_skip_suggested |
Skip the current suggestion and reveal the next |
dii_remove_ingredient |
Remove an ingredient (signals recalculation if essential) |
dii_add_manual |
Manually add a user-typed ingredient |
dii_clear_all |
Clear all selected ingredients from the session |
finalize_ingredient_session |
Commit session results to fridge and/or dish catalog |
dii_get_state |
Get current DII session state without modifying it |
All handlers follow the signature def handler(args: dict, **kwargs) -> str and return JSON strings.
See skill.md for detailed instructions on when and how an AI assistant should invoke each tool.
Each tool lives in its own module under src/handlers/ and exposes a HANDLER callable. Since the package uses relative imports, standalone invocation requires bootstrapping it via importlib:
# Get dinner suggestions based on current fridge contents
python3 -c "
import sys, importlib, pathlib
sys.path.insert(0, str(pathlib.Path('.').resolve().parent))
m = importlib.import_module('.src.handlers.get_meal_suggestions', pathlib.Path('.').resolve().name)
print(m.HANDLER({}))
"Swap get_meal_suggestions for any other module under src/handlers/, for example:
update_fridge_inventory.HANDLER({'action': 'add', 'ingredients': ['chicken', 'rice']})get_quick_shopping_list.HANDLER({})register_cooked_meal.HANDLER({'dish_name': 'rice with chicken'})
python3 test_integration.pyThis script seeds its own test data, exercises all nineteen tools end-to-end, and restores the original data files afterwards.
For the fastest feedback on pure domain logic, run python3 test_unit.py. It covers the dataclass, scoring, shopping, and ingredient-normalization helpers without touching data/.
meal-manager/
├── src/
│ ├── __init__.py # Package marker + atomic_write_json helper
│ ├── dish.py # Dish dataclass — recipe model (essential/optional ingredients)
│ ├── suggestion.py # Scoring engine — ranks dishes by availability + recency
│ ├── shopping.py # Shopping suggestions — single-ingredient unlock logic
│ ├── handlers/ # One module per registered tool (NAME, SCHEMA, HANDLER)
│ │ ├── __init__.py # iter_tools() walks the package and yields each triple
│ │ ├── _common.py # Shared helpers (err, normalization, input limits)
│ │ ├── get_meal_suggestions.py # Rank cookable dishes by availability and recency
│ │ ├── get_quick_shopping_list.py # Single-ingredient purchases that unlock new dishes
│ │ ├── update_fridge_inventory.py # Add or remove ingredients from the fridge
│ │ ├── register_cooked_meal.py # Log a dish as cooked and auto-remove essentials
│ │ ├── delete_history_entry.py # Undo a cooked-meal entry from history
│ │ ├── list_fridge.py # Return the current fridge contents
│ │ ├── add_dish.py # Add a new recipe to the catalog
│ │ ├── add_dishes_batch.py # Add multiple recipes in a single call
│ │ ├── delete_dish.py # Remove a recipe from the catalog
│ │ ├── edit_dish.py # Replace the ingredient list of an existing recipe
│ │ ├── clear_fridge.py # Empty the fridge inventory
│ │ ├── init_ingredient_session.py # Start a DII session with ranked suggestions
│ │ ├── dii_add_suggested.py # Accept the current DII suggestion and reveal the next
│ │ ├── dii_skip_suggested.py # Skip the current DII suggestion and reveal the next
│ │ ├── dii_remove_ingredient.py # Remove a DII ingredient (flags recalc if essential)
│ │ ├── dii_add_manual.py # Add a user-typed ingredient to the DII session
│ │ ├── dii_clear_all.py # Clear all selected ingredients in the DII session
│ │ ├── dii_get_state.py # Read-only DII session state query
│ │ └── finalize_ingredient_session.py # Commit the DII session to fridge and/or dish catalog
│ ├── repositories/ # Persistence layer behind Protocol seams
│ │ ├── __init__.py # Singletons + configure(data_dir)
│ │ ├── base.py # DishRepository / FridgeRepository / HistoryRepository
│ │ ├── json_dish.py # Recipe catalog persistence (data/dishes.json)
│ │ ├── json_fridge.py # Fridge inventory persistence (data/fridge.json)
│ │ └── json_history.py # Cooking history persistence (data/history.json)
│ └── dii/ # Dynamic Ingredient Interface
│ ├── __init__.py # Public API + configure(session_dir)
│ ├── session.py # DIISession dataclass + serialization
│ ├── store.py # In-memory map mirrored to data/sessions/ with TTL
│ ├── engine.py # Pure mutations on a DIISession
│ ├── presenter.py # LLM-facing response shape
│ └── finalizer.py # Commits a session via injected repositories
├── data/
│ ├── dishes.json # Recipe catalog (dishes with ingredients)
│ ├── fridge.json # Current fridge inventory (list of ingredients)
│ ├── history.json # Cooking history (dish name → last-cooked ISO date)
│ └── sessions/ # (created lazily) DII session backups for crash recovery
├── plugin.yaml # Hermes plugin manifest (name + provided tools)
├── __init__.py # Plugin entry point — register(ctx, *, data_dir=None)
├── test_unit.py # Unit tests for domain logic modules
├── test_integration.py # Integration smoke test
├── skill.md # Prompt instructions defining when/how to call each tool
├── AGENTS.md # Repository guidance for agentic coding work
├── CLAUDE.md # Development guidelines for Claude Code
├── LICENSE # GPLv3 license text
└── README.md # This file — project overview and usage guide
data/dishes.json — Recipe catalog:
{
"dishes": [
{
"name": "rice with chicken",
"ingredients": {
"rice": true,
"chicken": true,
"peppers": false
}
}
]
}true= essential ingredient (must be in the fridge to cook the dish)false= optional ingredient (improves the suggestion score but is not required)- Legacy
prep_timefields are ignored on load and are not written back.
data/fridge.json — Fridge inventory:
["potatoes", "eggs", "rice"]data/history.json — Cooking history:
{"rice with chicken": "2026-04-02"}Contributions are welcome. To get started:
- Fork the repository.
- Create a feature branch (
git checkout -b feature/my-feature). - Make your changes and verify them with
python3 test_unit.pyandpython3 test_integration.py. - Commit your changes and open a Pull Request.
Please ensure all ingredient and dish names follow the lowercase/stripped normalization convention used throughout the codebase.
This project is licensed under the GNU General Public License v3.0.
You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications to or software including (via compiler) GPL-licensed code must also be made available under the GPL along with build & install instructions.
See the LICENSE file for the full license text.