Recipe engine: dietary constraints, glycemic limiter, and nutrition panel #8

Closed
opened 2026-04-01 17:32:05 -07:00 by pyr0ball · 3 comments
Owner

Feature Request

Dietary constraint filters should apply at all 4 creativity levels, not just LLM levels.

Proposed design

Add dietary_constraints: list[str] to RecipeRequest. Map each constraint to:

Level 1/2 (deterministic corpus)
Pre-filter recipes via SQL exclusion lists:

  • low_sugar / diabetic_friendly → exclude recipes where ingredient_names contains: sugar, brown sugar, honey, corn syrup, maple syrup, molasses, condensed milk, etc. AND/OR include recipes with keywords containing diabetic, low-sugar, sugar-free
  • vegetarian → exclude: chicken, beef, pork, bacon, fish, shrimp, etc.
  • vegan → vegetarian exclusions + dairy/egg exclusions
  • gluten_free → exclude: flour, wheat, bread crumbs, pasta (unless gluten-free keyword)
  • dairy_free → exclude: milk, cream, cheese, butter, yogurt
  • low_sodium → exclude high-sodium ingredients; prefer recipes without added salt
  • nut_free → exclude: almonds, walnuts, peanuts, cashews, pecans, etc.

Level 3/4 (LLM)
Append to the existing constraints block already passed to the LLM prompt.

Trigger

User suggestion: diabetic user asked for low-sugar recipe options (2026-04-01).

Notes

  • The exclusion ingredient lists need to be curated carefully (e.g. rice sugar vs granulated sugar)
  • Consider a dietary_profiles table in the DB (per-user saved profiles) — already gated as Paid tier feature
  • Could eventually tie into ingredient_profiles.category for smarter filtering than string matching
## Feature Request Dietary constraint filters should apply at **all 4 creativity levels**, not just LLM levels. ### Proposed design Add `dietary_constraints: list[str]` to `RecipeRequest`. Map each constraint to: **Level 1/2 (deterministic corpus)** Pre-filter recipes via SQL exclusion lists: - `low_sugar` / `diabetic_friendly` → exclude recipes where `ingredient_names` contains: sugar, brown sugar, honey, corn syrup, maple syrup, molasses, condensed milk, etc. AND/OR include recipes with `keywords` containing `diabetic`, `low-sugar`, `sugar-free` - `vegetarian` → exclude: chicken, beef, pork, bacon, fish, shrimp, etc. - `vegan` → vegetarian exclusions + dairy/egg exclusions - `gluten_free` → exclude: flour, wheat, bread crumbs, pasta (unless gluten-free keyword) - `dairy_free` → exclude: milk, cream, cheese, butter, yogurt - `low_sodium` → exclude high-sodium ingredients; prefer recipes without added salt - `nut_free` → exclude: almonds, walnuts, peanuts, cashews, pecans, etc. **Level 3/4 (LLM)** Append to the existing `constraints` block already passed to the LLM prompt. ### Trigger User suggestion: diabetic user asked for low-sugar recipe options (2026-04-01). ### Notes - The exclusion ingredient lists need to be curated carefully (e.g. rice sugar vs granulated sugar) - Consider a `dietary_profiles` table in the DB (per-user saved profiles) — already gated as Paid tier feature - Could eventually tie into `ingredient_profiles.category` for smarter filtering than string matching
Author
Owner

Addendum: sugar substitute swaps

Instead of (or in addition to) hard-excluding recipes with sugar, the engine should suggest diabetic-friendly substitutes as swap_candidates when diabetic_friendly or low_sugar is active.

Substitute map (starting point)

Original Substitute Ratio Notes
granulated sugar erythritol 1:1 minimal aftertaste, best all-purpose
granulated sugar monk fruit sweetener 1:1 heat-stable, good for baking
granulated sugar stevia (baking blend) 1:1 brand-specific — pure stevia is ~200x sweeter
brown sugar erythritol + molasses 1:1 preserve moisture
honey sugar-free maple syrup 3/4:1 reduce liquids slightly
maple syrup sugar-free maple syrup 1:1 direct swap
corn syrup vegetable glycerin 1:1 or xylitol syrup
condensed milk sugar-free condensed milk 1:1 or coconut cream + sweetener
powdered sugar powdered erythritol 1:1 blend erythritol in food processor if needed

Integration points

  • Level 1/2: inject these as swap_candidates on matched recipes (same SwapCandidate model already used for substitution_pairs)
  • Level 3/4: include substitute map in the LLM system prompt when constraint is active, so it generates recipes using the substitutes natively
  • substitution_pairs table: seed these as a curated dietary-constraint-tagged subset so they appear in level 2 automatically

UX note

Recipes should not be excluded purely because they contain sugar — instead surface them with the swap annotation so users can still cook familiar dishes adapted to their needs.

## Addendum: sugar substitute swaps Instead of (or in addition to) hard-excluding recipes with sugar, the engine should suggest diabetic-friendly substitutes as `swap_candidates` when `diabetic_friendly` or `low_sugar` is active. ### Substitute map (starting point) | Original | Substitute | Ratio | Notes | |----------|-----------|-------|-------| | granulated sugar | erythritol | 1:1 | minimal aftertaste, best all-purpose | | granulated sugar | monk fruit sweetener | 1:1 | heat-stable, good for baking | | granulated sugar | stevia (baking blend) | 1:1 | brand-specific — pure stevia is ~200x sweeter | | brown sugar | erythritol + molasses | 1:1 | preserve moisture | | honey | sugar-free maple syrup | 3/4:1 | reduce liquids slightly | | maple syrup | sugar-free maple syrup | 1:1 | direct swap | | corn syrup | vegetable glycerin | 1:1 | or xylitol syrup | | condensed milk | sugar-free condensed milk | 1:1 | or coconut cream + sweetener | | powdered sugar | powdered erythritol | 1:1 | blend erythritol in food processor if needed | ### Integration points - **Level 1/2**: inject these as `swap_candidates` on matched recipes (same `SwapCandidate` model already used for substitution_pairs) - **Level 3/4**: include substitute map in the LLM system prompt when constraint is active, so it generates recipes using the substitutes natively - `substitution_pairs` table: seed these as a curated dietary-constraint-tagged subset so they appear in level 2 automatically ### UX note Recipes should not be excluded purely because they contain sugar — instead surface them with the swap annotation so users can still cook familiar dishes adapted to their needs.
pyr0ball changed title from Recipe engine: dietary constraints filter (all levels) to Recipe engine: dietary constraints, glycemic limiter, and nutrition panel 2026-04-01 17:38:42 -07:00
Author
Owner

User feedback — expanded to full nutrition panel

"I would be happy to see the total sugar values, fats and all the others in the list — a total esta tracker, what is in the food where possible. Some foods don't have it. A per-100-gram thing would be awesome — that also helps for all the gym jockey out there."

This is no longer just a diabetic filter. It is a nutrition panel feature that serves two clear audiences: health/diabetic users who need to limit specific macros, and fitness users who track macros precisely.


Revised spec

Data layer

Current state:

  • recipes table: calories, fat_g, protein_g — present on ~1,200 of 3.1M recipes (food.com corpus). No sugar, carbs, or sodium.
  • ingredient_profiles table: has fat_pct, protein_pct, starch_pct, sodium_mg_per_100g — no sugar field.
  • USDA FDC IDs stored in ingredient_profiles.usda_fdc_id — can be used to backfill sugar + full nutrition.

Pipeline additions needed (new migration + pipeline stage):

  • Add sugar_g_per_100g, carbs_g_per_100g to ingredient_profiles (backfill from USDA FDC API)
  • Add sugar_g, carbs_g, sodium_mg, fiber_g to recipes (estimate = Σ ingredient × quantity × per_100g ÷ 100)
  • Mark estimated values with a confidence flag (nutrition_estimated: bool) — show ~ prefix in UI
  • Per-100g values calculated from estimated total ÷ estimated total weight (sum of ingredient weights where parseable)

Recipe request

class RecipeRequest(BaseModel):
    ...
    max_sugar_g: float | None = None        # glycemic limiter
    max_carbs_g: float | None = None
    max_calories: int | None = None
    max_sodium_mg: float | None = None

Filter applied at level 1/2 via SQL WHERE sugar_g <= ? (NULL rows pass through — no data = not excluded). Injected as hard constraints at level 3/4 LLM prompt.

Recipe response — nutrition panel

class NutritionPanel(BaseModel):
    calories: float | None
    fat_g: float | None
    protein_g: float | None
    carbs_g: float | None
    sugar_g: float | None
    fiber_g: float | None
    sodium_mg: float | None
    per_100g: bool          # True = normalised per 100g; False = per serving
    estimated: bool         # True = calculated from ingredient profiles, not lab-verified

class RecipeSuggestion(BaseModel):
    ...
    nutrition: NutritionPanel | None   # None when no data available at all

UI

  • Nutrition chip row on each suggestion card: 🔥 Kcal · 🧈 Fat · 💪 Protein · 🍬 Sugar (greyed out when null)
  • ~ prefix on estimated values; tooltip: "Estimated from ingredient profiles — not lab-verified"
  • Filter panel: sliders for max sugar, max calories, max sodium; toggle per-serving ↔ per-100g
  • Dietary profiles (Paid): save named sets (e.g. "Diabetic", "Bulk", "Cut") — reuse across sessions

Audiences

  • Diabetic / low-GI: max_sugar_g slider (e.g. 10g) + swap_candidates for sugar substitutes (see previous comment)
  • Fitness / macro tracking: full panel display + per-100g toggle + max_calories / max_carbs sliders
  • Low-sodium / heart health: max_sodium_mg slider

Notes

  • Recipes without any nutrition data still appear — never silently exclude based on missing data
  • Confidence matters: distinguish food.com-provided values (higher trust) from ingredient-profile estimates (lower trust)
  • USDA FDC backfill can be a one-time pipeline run; ~378K ingredient profiles × FDC API (rate-limited but free)
## User feedback — expanded to full nutrition panel > "I would be happy to see the total sugar values, fats and all the others in the list — a total esta tracker, what is in the food where possible. Some foods don't have it. A per-100-gram thing would be awesome — that also helps for all the gym jockey out there." This is no longer just a diabetic filter. It is a **nutrition panel** feature that serves two clear audiences: health/diabetic users who need to limit specific macros, and fitness users who track macros precisely. --- ## Revised spec ### Data layer **Current state:** - `recipes` table: `calories`, `fat_g`, `protein_g` — present on ~1,200 of 3.1M recipes (food.com corpus). No sugar, carbs, or sodium. - `ingredient_profiles` table: has `fat_pct`, `protein_pct`, `starch_pct`, `sodium_mg_per_100g` — no sugar field. - USDA FDC IDs stored in `ingredient_profiles.usda_fdc_id` — can be used to backfill sugar + full nutrition. **Pipeline additions needed (new migration + pipeline stage):** - Add `sugar_g_per_100g`, `carbs_g_per_100g` to `ingredient_profiles` (backfill from USDA FDC API) - Add `sugar_g`, `carbs_g`, `sodium_mg`, `fiber_g` to `recipes` (estimate = Σ ingredient × quantity × per_100g ÷ 100) - Mark estimated values with a confidence flag (`nutrition_estimated: bool`) — show `~` prefix in UI - Per-100g values calculated from estimated total ÷ estimated total weight (sum of ingredient weights where parseable) ### Recipe request ```python class RecipeRequest(BaseModel): ... max_sugar_g: float | None = None # glycemic limiter max_carbs_g: float | None = None max_calories: int | None = None max_sodium_mg: float | None = None ``` Filter applied at level 1/2 via SQL `WHERE sugar_g <= ?` (NULL rows pass through — no data = not excluded). Injected as hard constraints at level 3/4 LLM prompt. ### Recipe response — nutrition panel ```python class NutritionPanel(BaseModel): calories: float | None fat_g: float | None protein_g: float | None carbs_g: float | None sugar_g: float | None fiber_g: float | None sodium_mg: float | None per_100g: bool # True = normalised per 100g; False = per serving estimated: bool # True = calculated from ingredient profiles, not lab-verified class RecipeSuggestion(BaseModel): ... nutrition: NutritionPanel | None # None when no data available at all ``` ### UI - Nutrition chip row on each suggestion card: 🔥 Kcal · 🧈 Fat · 💪 Protein · 🍬 Sugar (greyed out when null) - `~` prefix on estimated values; tooltip: "Estimated from ingredient profiles — not lab-verified" - Filter panel: sliders for max sugar, max calories, max sodium; toggle per-serving ↔ per-100g - Dietary profiles (Paid): save named sets (e.g. "Diabetic", "Bulk", "Cut") — reuse across sessions ### Audiences - **Diabetic / low-GI**: max_sugar_g slider (e.g. 10g) + swap_candidates for sugar substitutes (see previous comment) - **Fitness / macro tracking**: full panel display + per-100g toggle + max_calories / max_carbs sliders - **Low-sodium / heart health**: max_sodium_mg slider ### Notes - Recipes without any nutrition data still appear — never silently exclude based on missing data - Confidence matters: distinguish food.com-provided values (higher trust) from ingredient-profile estimates (lower trust) - USDA FDC backfill can be a one-time pipeline run; ~378K ingredient profiles × FDC API (rate-limited but free)
Author
Owner

Closing — dietary constraints (vegan/vegetarian/gluten-free/dairy-free/etc.), allergy list, and per-serving nutrition panel (kcal, fat, protein, carbs, fiber, sugar, sodium) are all wired. Nutrition filters (max_calories, max_sugar_g, max_carbs_g, max_sodium_mg) implemented in NutritionFilters schema. Texture profiles backfilled from macro data for 378K ingredient_profiles rows.

Closing — dietary constraints (vegan/vegetarian/gluten-free/dairy-free/etc.), allergy list, and per-serving nutrition panel (kcal, fat, protein, carbs, fiber, sugar, sodium) are all wired. Nutrition filters (max_calories, max_sugar_g, max_carbs_g, max_sodium_mg) implemented in NutritionFilters schema. Texture profiles backfilled from macro data for 378K ingredient_profiles rows.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Circuit-Forge/kiwi#8
No description provided.