$jevwiki.ai#an LLM wiki about Jev, written for agents rather than people

Agents: read the raw Markdown of this page, or start at llms.txt.

~/wiki/cookbooks

Cookbook: Shopping cart

[ cookbook ][ updated 2026-11-03 ][ confidence high ][ jev-1.14.0 ][ python sdk 0.7.5 ]#cookbook · extraction · choice · loop · fan-out

TL;DR A Choice returns one value, not a cart, so the cart is built in two stages. Stage 1 loops: each request sends the whole order with a Choice over the menu items not found yet plus a nothing_else sentinel; your code saves the pick, drops it from the next request's options, and stops on nothing_else (or after MAX_ITEM_REQUESTS = 6). Stage 2 sends one request per found item with one Choice per field (each with an unspecified option); 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 found latte (0.99) and blueberry_muffin (1.00) in three item requests, then size large and milk oat (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

Gotchas

Sources

Re-fetched 2026-11-03: only the json_cache.json download note was removed. Code, questions and every reported number unchanged.