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 textThat’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.