Agents: read the raw Markdown of this page, or start at llms.txt.
Cookbook: Shopping cart
TL;DR A
Choicereturns one value, not a cart, so the cart is built in two stages. Stage 1 loops: each request sends the whole order with aChoiceover the menu items not found yet plus anothing_elsesentinel; your code saves the pick, drops it from the next request's options, and stops onnothing_else(or afterMAX_ITEM_REQUESTS = 6). Stage 2 sends one request per found item with oneChoiceper field (each with anunspecifiedoption); items with no fields need no request. On "Can I get a large oat milk latte and a blueberry muffin please" (jev-1.14.0, 2026-10-29) it foundlatte(0.99) andblueberry_muffin(1.00) in three item requests, thensizelargeandmilkoat(1.00 each).
Goal
Turn one natural-language order into a structured cart with separate items and fields, using only single-value answers. The demo behind it is TypeSafe's shopping-cart video (linked from the cookbook); the cookbook runs the TypeSafe part on a four-item cafe menu and one hard-coded order:
latte
size: large
milk: oat
blueberry_muffin
Upstream lists it under Extraction, level Beginner (Cookbooks overview). It shows two things an agent building with Jev needs often: how to find an unknown number of things with a question that returns one answer (a shrinking-options loop), and how to fill several known fields at once (Speculative fan-out).
When to use it: a fixed catalog (menu, product list, form schema) whose items and field values you can list as options, and inputs that mention a handful of them. When not to: free-form values (names, addresses, arbitrary numbers) that you cannot enumerate as options, or quantities and prices you need to compute (see Gotchas; arithmetic is a documented weak spot, Model jaggedness: known failure modes).
Inputs / state shape
The state of every request is the order string itself, unchanged from request to request; only the question changes (State: what you send Jev):
Can I get a large oat milk latte and a blueberry muffin please
The menu lives in code. Each item's description carries the words a customer would use, and fields is the item's own schema: each field is a list of allowed values, which become that field's Choice options. Only the latte has fields.
MENU = {
"latte": {
"description": "Espresso with steamed milk. Also ordered as a flat white or a milky coffee.",
"fields": {
"size": ["small", "medium", "large"],
"milk": ["whole", "oat", "almond"],
},
},
"filter_coffee": {
"description": "Plain brewed drip coffee, sometimes ordered as a black coffee.",
"fields": {},
},
"blueberry_muffin": {
"description": "A blueberry muffin.",
"fields": {},
},
"almond_croissant": {
"description": "An almond croissant, sometimes ordered as a pastry.",
"fields": {},
},
}
ORDER = "Can I get a large oat milk latte and a blueberry muffin please"
Constants: TYPESAFE_MODEL = "jev-1.14.0", NOTHING_ELSE = "nothing_else", MAX_ITEM_REQUESTS = 6, UNSPECIFIED = "unspecified"; the client is built with model=TYPESAFE_MODEL and timeout=30.0. Numbers below came from jev-1.14.0 on 2026-10-29 (the cookbook's first capture, 2026-10-31; no earlier run).
Questions asked
Stage 1, one per request (key item):
| key | type | instructions |
criteria |
|---|---|---|---|
item |
Choice |
This is a cafe order. Items already accounted for: {already}. Which remaining menu item does the order mention? If the order mentions more than one remaining item, choose the one mentioned first. ({already} is the found names joined with ", ", or nothing yet) |
one option per remaining menu item, its description as the criteria; plus nothing_else: "The order mentions no menu item other than the ones already accounted for." |
A Choice must select an option, so the sentinel is what lets the loop stop.
Stage 2, one request per item with fields (one key per field, here size and milk):
| key | type | instructions |
criteria |
|---|---|---|---|
{field} |
Choice |
Which {field} did the order ask for, for the {item_name}? |
each allowed value with None as its description; plus unspecified: "The order does not say which {field} for the {item_name}." |
unspecified means a field the customer did not mention gets that option instead of a forced concrete value.
Combining logic in code
Upstream code, verbatim except the setup lines folded in:
import os
from pathlib import Path
from cooksafe import JsonCache
from typesafe_sdk import Choice, TypeSafeClient
TYPESAFE_MODEL = "jev-1.14.0"
NOTHING_ELSE = "nothing_else" # the sentinel option that ends the item loop
MAX_ITEM_REQUESTS = 6 # a hard stop, so a bad answer can never loop forever
client = TypeSafeClient(
api_key=os.environ.get("TYPESAFE_API_KEY", "cache-only"), # cached re-renders need no key
model=TYPESAFE_MODEL,
timeout=30.0,
)
json_cache = JsonCache(Path("json_cache.json"))
def fmt_confidence(confidence: float | None) -> str:
"""A confidence for printing. The API can return None, so never format it blind."""
return f"{confidence:.2f}" if confidence is not None else "n/a"
# MENU and ORDER as in "Inputs / state shape" above
def item_question(remaining: dict[str, dict], found: list[str]) -> Choice:
"""Build one request's question from the items not found yet."""
already = ", ".join(found) if found else "nothing yet"
criteria = {name: item["description"] for name, item in remaining.items()}
criteria[NOTHING_ELSE] = (
"The order mentions no menu item other than the ones already accounted for."
)
return Choice(
instructions=(
f"This is a cafe order. Items already accounted for: {already}. "
"Which remaining menu item does the order mention? If the order mentions more "
"than one remaining item, choose the one mentioned first."
),
criteria=criteria,
)
@json_cache
def ask_item(order: str, question: dict) -> dict:
"""One item request. The question arrives as its API JSON, which also keys the cache."""
answer = client.system_one(state=order, questions={"item": question}).choices["item"]
return {"choice": answer.choice, "confidence": answer.confidence}
def find_items(order: str) -> list[dict]:
remaining = list(MENU)
found: list[dict] = []
item_requests = 0
for _ in range(MAX_ITEM_REQUESTS):
if not remaining:
break
question = item_question(
{name: MENU[name] for name in remaining},
[item["name"] for item in found],
)
result = ask_item(order, question.model_dump(mode="json", exclude_none=True))
item_requests += 1
print(
f"item request {item_requests}: {len(remaining)} items + sentinel "
f"-> {result['choice']} (confidence {fmt_confidence(result['confidence'])})"
)
if result["choice"] == NOTHING_ELSE:
break
remaining.remove(result["choice"])
found.append({"name": result["choice"], "confidence": result["confidence"]})
return found
UNSPECIFIED = "unspecified"
def field_questions(item_name: str) -> dict[str, Choice]:
questions = {}
for field, values in MENU[item_name]["fields"].items():
criteria = {value: None for value in values}
criteria[UNSPECIFIED] = (
f"The order does not say which {field} for the {item_name}."
)
questions[field] = Choice(
instructions=f"Which {field} did the order ask for, for the {item_name}?",
criteria=criteria,
)
return questions
@json_cache
def ask_fields(order: str, questions: dict[str, dict]) -> dict:
"""One field request. The questions arrive as their API JSON, which also keys the cache."""
response = client.system_one(state=order, questions=questions)
return {
field: {"choice": answer.choice, "confidence": answer.confidence}
for field, answer in response.choices.items()
}
def fill_fields(order: str, item_name: str) -> dict:
if not MENU[item_name]["fields"]:
return {}
questions = {
field: question.model_dump(mode="json", exclude_none=True)
for field, question in field_questions(item_name).items()
}
return ask_fields(order, questions)
items = find_items(ORDER)
cart = []
for item in items:
fields = fill_fields(ORDER, item["name"])
cart.append(
{"name": item["name"], "confidence": item["confidence"], "fields": fields}
)
The loop ends on any of three conditions: the sentinel is selected, the menu runs out, or the cap is reached. Each found item leaves the option list, so it can be found at most once. Assembling and printing the cart makes no TypeSafe calls; it only reads the stored single-value answers.
Results / what the cookbook reports
All numbers from jev-1.14.0 (2026-10-29), one order:
item request 1: 4 items + sentinel -> latte (confidence 0.99)
item request 2: 3 items + sentinel -> blueberry_muffin (confidence 1.00)
item request 3: 2 items + sentinel -> nothing_else (confidence 1.00)
found 2 items: ['latte', 'blueberry_muffin']
order: "Can I get a large oat milk latte and a blueberry muffin please"
latte (item confidence 0.99)
size large confidence 1.00
milk oat confidence 1.00
blueberry_muffin (item confidence 1.00)
That is four requests in total for this order: three item requests and one field request for the latte; the muffin has no fields (count inferred from the code and output). The cookbook reports no accuracy over a set of orders, no latency and no cost; it is a single worked example. It also builds a playground link (make_playground_link(ORDER, {"item": item_question(MENU, [])}); it also passed models=[TYPESAFE_MODEL] until the 2026-11-02 docs) holding the first item request, so you can edit the order and watch the answer change.
Adapting it to a new domain
- Replace
MENUwith your catalog. Put the words customers actually use in eachdescription(the latte's lists "flat white" and "milky coffee"); Jev matches the input against these (Writing instructions and criteria that Jev reads correctly). - Add fields per item as lists of allowed values. For quantities, the cookbook's own advice: add a
quantityfield to each item's schema, with values such asone,twoandthree. - Keep
MAX_ITEM_REQUESTSabove your largest realistic order plus one (the sentinel request). - Read the confidences you already get: the example prints them but never acts on them. For a real order, route low-confidence items or fields to a confirmation step (Confidence-gated routing, Confidence vs probability).
- The same two-step shape turns a sentence into a function call: Cookbook: Function calling picks the function with one
Choiceand fills each argument with one, but handles an argument the user did not mention with a separate yes/no question instead of anunspecifiedoption.
Gotchas
- No quantities, no duplicates. The cart has no quantity field, so "two flat whites" still produces one
latte, and the loop finds each menu item at most once (two lattes with different milks cannot be expressed as written). - Stage 1 is sequential. Each item request depends on the previous answer, so an order with n items costs n + 1 round trips before any field request; only the field questions of one item go out together. Cookbook: Parallel questions measures what batching saves.
- A request is needed even to stop. The sentinel request always runs unless the menu runs out, so even a one-item order takes two item requests.
- Confidence can be
None. The cookbook'sfmt_confidenceguards against it ("The API can return None, so never format it blind"); do the same in any threshold check. - Cache key.
ask_itemandask_fieldsare cached on the order plus the question JSON; the model is not an argument, so a cached answer from one model replays under another (inferred from the decorated functions' arguments). Movejson_cache.jsonaside when you changeTYPESAFE_MODEL. - Install line.
pip install ipython 'cooksafe>=0.2.0,<0.3.0', then setTYPESAFE_API_KEY. Calls are memoized to a localjson_cache.json; since the 2026-11-03 docs the cookbook no longer offers a published cache intypesafe-public-examplesto replay its numbers (the key still defaults to"cache-only"in the sample, which only helps once your own cache is filled; inferred).
Related
- Choice questions — the only primitive used; one answer per question
- Speculative fan-out — the field questions of one item in one request
- Cookbook: Function calling — the same pick-then-fill shape for function calls
- Cookbook: Parallel questions — what one batched request saves over one at a time
- Cookbook: Hierarchical classification — another flow where each answer sets the next request's options
- Primitives: Choice, Score, Noul — when a second request is justified
- Cookbooks overview — the full cookbook catalog
Sources
- raw/docs/cookbooks__cart_from_primitives.md (https://docs.typesafe.ai/cookbooks/cart_from_primitives.md), first captured 2026-10-31
- raw/docs/cookbooks.md (https://docs.typesafe.ai/cookbooks.md): section and level
Re-fetched 2026-11-03: only the json_cache.json download note was removed. Code, questions and every reported number unchanged.