Chapters: 

Connect the Smart Ingredients Project to Get Smart

Get Smart is a controlled decision layer that consumes structured ingredient data from the Smart Ingredients Project and produces deterministic, explainable outputs.

Database (Backdrop / source tables)
        ↓
build_runtime_catalog.py
        ↓
runtime_recipes.json
        ↓
recipe_catalog.py
        ↓
Runtime (chatbot / nutrition engine)
 

Let us wire

Smart Ingredients (data + signals)
        ↓
Get Smart (decision system)

aka

Backdrop (recipes, ingredients, structure)
        ↓
Gateway (contract + enforcement)
        ↓
Python (Smart Ingredients logic + nutrition decisions)
        ↓
Structured output

 


🔗 Get Smart — Integration Contract

Two systems

The Nutrition Project from end of March 2026

Found it

[CML] tux@localhost …/ai-agents-crash-course/multi_agent_chatbot $ cat recipe_catalog.py
"""Runtime recipe catalog loaded from structured ingestion output."""

from __future__ import annotations

import json
from copy import deepcopy
from pathlib import Path
from typing import Any, Dict, Iterable


CATALOG_PATH = Path(__file__).resolve().with_name("runtime_recipes.json")
PREFERRED_DEFAULT_TITLE = "black eyed peas recipe (greek-style)"


def _load_catalog() -> dict[str, dict[str, Any]]:
    if not CATALOG_PATH.exists():
        raise FileNotFoundError(
            f"Runtime catalog not found: {CATALOG_PATH}. Run build_runtime_catalog.py first."
        )
    data = json.loads(CATALOG_PATH.read_text(encoding="utf-8"))
    catalog: dict[str, dict[str, Any]] = {}
    for entry in data:
        entry_id = entry.get("id")
        if not entry_id:
            continue
        catalog[entry_id] = {
            "id": entry_id,
            "title": entry.get("title"),
            "servings": entry.get("servings", 2),
            "ingredients": entry.get("ingredients", []),
            "ingredient_lines_raw": entry.get("ingredient_lines_raw", []),
        }
    if not catalog:
        raise ValueError("Runtime catalog is empty; no recipes loaded.")
    return catalog


def _sorted_recipe_ids(entries: Iterable[dict[str, Any]]) -> list[str]:
    sortable = []
    for entry in entries:
        title = (entry.get("title") or "").strip().lower()
        sortable.append((title, entry["id"]))
    sortable.sort()
    return [item[1] for item in sortable]


_RUNTIME_CATALOG = _load_catalog()
_SORTED_IDS = _sorted_recipe_ids(_RUNTIME_CATALOG.values())
preferred = next(
    (
        recipe_id
        for recipe_id in _SORTED_IDS
        if (_RUNTIME_CATALOG[recipe_id].get("title") or "").strip().lower()
        == PREFERRED_DEFAULT_TITLE
    ),
    None,
)
DEFAULT_RECIPE_ID = preferred or (_SORTED_IDS[0] if _SORTED_IDS else next(iter(_RUNTIME_CATALOG.keys())))


def get_recipe(recipe_id: str | None) -> Dict[str, Any]:
    recipe = _RUNTIME_CATALOG.get(recipe_id or DEFAULT_RECIPE_ID)
    if not recipe:
        raise KeyError(f"Unknown recipe id: {recipe_id}")
    return deepcopy(recipe)


def list_recipes() -> list[dict[str, Any]]:
    return [deepcopy(_RUNTIME_CATALOG[recipe_id]) for recipe_id in _SORTED_IDS]


def resolve_recipe_id_by_title(title: str | None) -> str | None:
    if not title:
        return None
    target = title.strip().lower()
    if not target:
        return None
    for recipe_id, recipe in _RUNTIME_CATALOG.items():
        recipe_title = (recipe.get("title") or "").strip().lower()
        if recipe_title == target:
            return recipe_id
    return None


__all__ = ["get_recipe", "list_recipes", "DEFAULT_RECIPE_ID", "

Output


 {
   "id": "8244439496752c3b853b440b8ae0d26ecca5ec74a157ebbc54388967f77c4e58",
   "title": "Roasted Veggie and Black Bean Rice Bowls",
   "servings": 2,
   "ingredients": [
     {
       "name": "olive oil",
       "quantity": 1.0,
       "unit": "tbsp"
     },
     {
       "name": "garlic",
       "quantity": 2.0,
       "unit": "clove"
     },
     {
       "name": "medium sweet potatoes",
       "quantity": 2.0,
       "unit": null
     },
     {
       "name": "medium red onion",
       "quantity": 1.0,
       "unit": null
     },
     {
       "name": "large red bell pepper",
       "quantity": 1.0,
       "unit": null
     },
     {
       "name": "jalapeno",
       "quantity": 1.0,
       "unit": null
     },
     {
       "name": "cooked brown rice (try cilantro-lime rice)",
       "quantity": 3.0,
       "unit": "cup"
     },
     {
       "name": "black beans",
       "quantity": 2.0,
       "unit": "can"
     }
   ],
   "ingredient_lines_raw": [
     "    1 tablespoon olive oil",
     "    2 cloves garlic, minced",
     "       Dried oregano (or fresh oregano sprigs)",
     "       Fresh thyme or rosemary sprigs (they hold up to the heat)",
     "    2 medium sweet potatoes, diced",
     "    1 medium red onion, diced",
     "    1 large red bell pepper, diced",
     "    1 jalapeno, seeded and chopped fine",
     "    3 cups cooked brown rice (try cilantro-lime rice)",
     "    2 cans black beans, drained and rinsed",
     "    Lime wedges",
     "    Other toppings (avocado, chopped cilantro, sour cream, cheese, etc.)"
   ]
 }

Good. This is clean. No archaeology needed. Let’s lock the contract.


🧠 What recipe_catalog.py actually enforces

Your loader is not passive. It’s a gatekeeper with a whitelist mindset.

Everything coming from runtime_recipes.json gets reduced to this exact shape:

{
  "id": str,
  "title": str | None,
  "servings": int (default 2),
  "ingredients": list,
  "ingredient_lines_raw": list
}

Anything else in the JSON?

🗑️ quietly discarded

No drama. Just gone.


🔍 Your sample vs the contract

Your sample fits perfectly. That’s the important part.

✅ Survives ingestion

"id"
"title"
"servings"
"ingredients"
"ingredient_lines_raw"

🧩 Subtle but important behaviors

1. Default servings

If Backdrop omits it:

entry.get("servings", 2)

→ system silently assigns 2

That’s not neutral. That’s a decision.


2. Ingredients are trusted, not validated

This line:

"ingredients": entry.get("ingredients", [])

means:

  • No schema validation
  • No unit enforcement
  • No normalization here

👉 This file assumes upstream already did the thinking


3. Raw lines preserved separately

You kept both:

ingredients              ← structured truth
ingredient_lines_raw     ← original text

That’s deliberate and powerful.

It gives you:

  • deterministic math (ingredients)
  • interpretive fallback (raw lines)

⚠️ One constraint you haven’t hit yet

This line:

catalog[entry_id] = { ... }

means:

👉 id must be globally unique

If Backdrop sends duplicates:

  • last one wins
  • no warning
  • silent overwrite

🎯 What recipe_catalog expects from Backdrop

Not theory. Exact contract:

[
  {
    "id": "string (unique, required)",
    "title": "string",
    "servings": number (optional),
    "ingredients": [
      {
        "name": "string",
        "quantity": number,
        "unit": "string | null"
      }
    ],
    "ingredient_lines_raw": ["string"]
  }
]

That’s it.

No more. No less (for runtime survival).


⚡ What this means for your reconnection

You don’t need to “adapt the engine.”

You only need Backdrop to emit this exact shape.

Everything else in your system already aligns.


🧭 The real seam (the only one that matters)

Right here:

data = json.loads(CATALOG_PATH.read_text(...))

Replace file read with:

data = fetch_from_backdrop_api()

…and the engine doesn’t even notice.

That’s how tight this is.


 

 

 

 


🧭 Final anchor

You were right earlier, but now it’s concrete:

runtime_recipes.json is not just data
it is a filtered, opinionated contract

✔️ Step complete

You now know:

  • exactly what shape is required
  • exactly what is ignored
  • exactly where to swap in Backdrop

Next step, when you're ready:

👉 show how Backdrop currently exports (even rough JSON)

We’ll snap it to this contract without touching the engine.