mealie: add recipe-add/edit reference covering ingredient parser
Captures the non-obvious bits of Mealie's REST API: two-step create+patch flow, food/unit id resolution (parser returns id:null for unknown names), locale seeding to stop the parser fuzzy-matching teaspoon→tablespoon, required ingredientReferences:[] on instructions, and the family meal-type tagging convention (Breakfast/Lunch/Dinner/Dessert). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,8 @@ name: home
|
||||
description: >
|
||||
Activate this skill for any work related to the Fritz household (Hawks Nest).
|
||||
TRIGGER when: user mentions Hawks Nest, boat, MasterCraft, Minnetrista, Hawks Point,
|
||||
home automation, Home Assistant, Lutron, Z-Wave, lights, automations, 4400 Hawks.
|
||||
home automation, Home Assistant, Lutron, Z-Wave, lights, automations, 4400 Hawks,
|
||||
Mealie, meals, recipes, meal planning, meals.vino.network.
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
@@ -11,6 +12,7 @@ user-invocable: true
|
||||
- Address: 4400 Hawks Pt, Minnetrista, MN 55331
|
||||
- Names: Home, The Hawks Nest
|
||||
- Home Assistant: home.vino.network (HAOS on msp001 VM 123) — see fritzlab skill for access
|
||||
- Mealie: https://meals.vino.network (sjc001 cluster, `mealie` namespace, group `Home` / household `Family`)
|
||||
</quick-context>
|
||||
|
||||
<safety>
|
||||
@@ -28,4 +30,8 @@ user-invocable: true
|
||||
summary="Boat details: Stranger Fins, 2025 MasterCraft 21 — specs and info"
|
||||
categories=["boat", "fun", "entertainment"]
|
||||
keywords=["MasterCraft", "NXT", "boat", "Stranger Fins", "wake", "surf"] />
|
||||
<reference file="reference/mealie.md"
|
||||
summary="Mealie REST API: add/edit recipes, ingredient parser (food/unit id resolution + locale seeding), tag/category and instruction gotchas, misleading-error decoder"
|
||||
categories=["food", "services"]
|
||||
keywords=["Mealie", "meals.vino.network", "recipe", "recipes", "meal", "meal planning", "ingredient parser", "PATCH recipe", "MEALIE_TOKEN", "household Family"] />
|
||||
</references>
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
---
|
||||
name: mealie
|
||||
description: Adding and editing recipes in the family Mealie instance, including the non-obvious ingredient-parsing requirements
|
||||
---
|
||||
|
||||
<instance>
|
||||
- URL: https://meals.vino.network (Mealie v3.18.x, OIDC via Authentik)
|
||||
- Hosted in sjc001 cluster, `mealie` namespace
|
||||
- Family group/household: `Home` / `Family`
|
||||
- API token: stored in `code/git/code.fritzlab.net/fritzlab/agent/.env` as
|
||||
`MEALIE_TOKEN` (paired with `MEALIE_URL=https://meals.vino.network`).
|
||||
Regenerate at https://meals.vino.network/user/profile/api-tokens.
|
||||
- All endpoints take `Authorization: Bearer $MEALIE_TOKEN`.
|
||||
</instance>
|
||||
|
||||
<conventions>
|
||||
- **Every recipe must carry exactly one meal-type tag**: `Breakfast`, `Lunch`, `Dinner`,
|
||||
or `Dessert`. When importing or editing a recipe, add the appropriate one; preserve
|
||||
any other tags already on the recipe. If meal type is ambiguous (e.g. banana bread →
|
||||
breakfast or dessert), ask the user — don't pick silently.
|
||||
- Meal-type tag IDs in the family Mealie:
|
||||
- `Breakfast` = `38fb71bf-a89d-45ee-9d21-0b1e5f716569`
|
||||
- `Lunch` = `5f0ce972-2243-4ef6-b3c7-ad24f8561e84`
|
||||
- `Dinner` = `bc992ba9-80ce-4407-8d1e-e69a7f659ebb`
|
||||
- `Dessert` = `e7adc953-3f23-4032-804b-ada18b3a2da9`
|
||||
- To append a tag without clobbering: GET the recipe, take `tags`, append the new
|
||||
`{id, name, slug}` object, PATCH back with `{"name": <name>, "tags": [...]}`.
|
||||
</conventions>
|
||||
|
||||
<auth-test>
|
||||
GET /api/users/self — confirms the token works and returns group/household.
|
||||
</auth-test>
|
||||
|
||||
<add-from-url>
|
||||
Mealie scrapes recipe sites that publish schema.org Recipe JSON-LD.
|
||||
POST /api/recipes/create/html-or-json with `{"url":"https://..."}` — returns the new slug.
|
||||
Falls back to scraping HTML if no JSON-LD. PDFs and image URLs do NOT work here.
|
||||
</add-from-url>
|
||||
|
||||
<add-from-raw>
|
||||
Two-step: create then patch. Mealie has no single endpoint that takes a full recipe at once.
|
||||
|
||||
1. POST /api/recipes body `{"name": "..."}`
|
||||
Returns the slug as a **raw JSON string** (e.g. `"my-recipe"`), not an object.
|
||||
If a recipe with the same name exists, Mealie auto-suffixes (`-1`, `-2`). To avoid
|
||||
duplicates, GET /api/recipes first and match by name before posting.
|
||||
|
||||
2. PATCH /api/recipes/{slug} with the full Recipe-Input body:
|
||||
- `name`, `description`, `prepTime`, `cookTime`, `totalTime` (free strings)
|
||||
- `recipeYield` (string), `recipeServings` (number), `recipeYieldQuantity` (number)
|
||||
- `recipeIngredient: [...]` — see ingredient-parsing below
|
||||
- `recipeInstructions: [{text, ingredientReferences: []}, ...]`
|
||||
- `tags: [{id, name, slug}, ...]`, `recipeCategory: [{id, name, slug}, ...]`
|
||||
- `notes: [{title, text}, ...]`
|
||||
- `orgURL: "..."` — source link
|
||||
- DO NOT include `slug` in the PATCH body; it triggers a rename attempt that conflicts
|
||||
with the current slug and returns 400 "Recipe already exists".
|
||||
</add-from-raw>
|
||||
|
||||
<edit-existing>
|
||||
Same as step 2 above: PATCH /api/recipes/{slug} with the fields to change. Other fields
|
||||
are preserved. To replace the entire ingredient list, send the full new `recipeIngredient`
|
||||
array. To clear a list, send `[]`.
|
||||
|
||||
DELETE /api/recipes/{slug} removes the recipe.
|
||||
</edit-existing>
|
||||
|
||||
<ingredient-parsing>
|
||||
Mealie has an NLP parser (also `brute` and `openai` variants) that splits raw strings like
|
||||
`"1½ tablespoons mayo"` into structured `{quantity, unit, food, note}` objects. The unit
|
||||
and food are objects with their own `id` referencing rows in `/api/units` and `/api/foods`.
|
||||
|
||||
POST /api/parser/ingredients
|
||||
body: `{"parser":"nlp", "ingredients":["1½ tablespoons mayo", "4 ears fresh corn, husked", ...]}`
|
||||
returns one ParsedIngredient per input.
|
||||
|
||||
Parser gotchas that bite every time:
|
||||
|
||||
1. **Unit / food objects with `id: null` cannot be PATCHed onto a recipe** — server raises
|
||||
`ValueError: Expected 'id' to be provided for unit/food`. The parser returns `id: null`
|
||||
whenever the unit/food name isn't already in the database. Workflow: after parsing,
|
||||
resolve each name against `/api/units` and `/api/foods` (auto-creating missing ones via
|
||||
POST), then set `ing.unit.id` and `ing.food.id` before PATCHing the recipe.
|
||||
|
||||
2. **Seed the locale before parsing** or the parser fuzzy-matches against whatever you
|
||||
created previously. Concrete bite: with only `tablespoon` in the unit table, every
|
||||
"1 teaspoon" parses to `unit_id = <tablespoon's id>` because the Levenshtein distance
|
||||
is 1. Pre-seed once per instance:
|
||||
- POST /api/groups/seeders/units `{"locale":"en-US"}`
|
||||
- POST /api/groups/seeders/foods `{"locale":"en-US"}`
|
||||
These populate the standard US units (cup, teaspoon, tablespoon, ounce, etc.) and
|
||||
common foods. After seeding the parser stops conflating teaspoon/tablespoon. Already
|
||||
done on the family Mealie 2026-05-22.
|
||||
|
||||
3. **Sections in an ingredient list** (e.g. "Cookie dough" vs "Buttercream" in the same
|
||||
recipe): set `title` on the FIRST ingredient of each section. Convention used in the
|
||||
family import script: prefix the raw input with `[Section Name]`, the helper strips
|
||||
the bracket and promotes it to `title` on the first ingredient of that group.
|
||||
|
||||
4. **Instructions also have a hidden required field**: each step object must include
|
||||
`ingredientReferences: []`. The OpenAPI schema says it defaults to `[]`, but PATCH's
|
||||
`model_dump(exclude_unset=True)` excludes it and the SQLAlchemy model then raises
|
||||
`RecipeInstruction.__init__() missing 1 required positional argument: 'ingredient_references'`.
|
||||
Always send `ingredientReferences: []` explicitly.
|
||||
</ingredient-parsing>
|
||||
|
||||
<tags-categories>
|
||||
RecipeTag and RecipeCategory objects in a PATCH body need **all three** of `id`, `name`,
|
||||
`slug`. Sending just name+slug produces a misleading 400 "Recipe already exists" (the
|
||||
real cause is a SQL integrity error logged server-side as `SQL Integrity Error on recipe
|
||||
controller action`).
|
||||
|
||||
Create with POST /api/organizers/tags or POST /api/organizers/categories — body
|
||||
`{"name":"..."}` — response contains the `id` and `slug` to reuse.
|
||||
|
||||
Existing IDs in the family Mealie:
|
||||
- Tag `Mexican Street Eats Class` = `bb6841c5-3b6d-4569-b707-3d14bea83d7b`
|
||||
- Category `Mexican` = `6ee64be6-83ce-4d6e-926c-62edfb791a41`
|
||||
</tags-categories>
|
||||
|
||||
<misleading-errors>
|
||||
- `400 "Recipe already exists"` on PATCH → almost always means tag/category/food/unit
|
||||
is missing an `id`. Check the server logs (`kubectl --context sjc001 -n mealie logs
|
||||
-l app=mealie`) for `SQL Integrity Error` to confirm.
|
||||
- `500 TypeError` on PATCH → missing `ingredientReferences: []` on an instruction.
|
||||
- `500 ValueError: Expected 'id' to be provided for unit` → parsed ingredient still has
|
||||
`unit.id: null` or `food.id: null`; resolve via /api/units, /api/foods first.
|
||||
- `500` on POST /api/foods or /api/units when the name already exists → list and dedupe
|
||||
before creating, or catch the UniqueViolation.
|
||||
</misleading-errors>
|
||||
|
||||
<reusable-import-script>
|
||||
A working ingest script for the Mexican Street Eats PDF lives at
|
||||
`/tmp/add_mealie_recipes.py` during the 2026-05-22 session. Pattern to reuse:
|
||||
|
||||
1. Build food + unit name→id caches once (GET /api/foods, /api/units, paginate).
|
||||
2. For each raw ingredient line, POST to /api/parser/ingredients.
|
||||
3. For each parsed result with `unit.id: null` or `food.id: null`, look up by lowercased
|
||||
name in the cache; POST to create if missing; update cache.
|
||||
4. For sectioned recipes, strip `[Section]` prefix and set `title` on first item.
|
||||
5. Build PATCH body with `recipeIngredient`, `recipeInstructions` (each with
|
||||
`ingredientReferences: []`), `tags` and `recipeCategory` (with ids).
|
||||
6. POST to /api/recipes for the name, then PATCH /api/recipes/{slug} with the body.
|
||||
</reusable-import-script>
|
||||
Reference in New Issue
Block a user