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

Cookbook: Hierarchical classification

[ cookbook ][ updated 2026-09-17 ][ confidence high ][ jev-1.13.0 ][ python sdk 0.6.0 ]#cookbook · classification · choice · beam-search · taxonomy

TL;DR One Choice per sibling set, with criteria mapping opaque keys (c0, c1, …) to the child labels; the answer's probabilities are the edges. Score a path by product(edge_probabilities) ** (1 / decisions) and keep the best K = 3 — beam search matched 4 of 4 expected leaves where greedy matched 2 of 4.

Goal

Classify a document into a deep hierarchy by traversing it node by node to the correct leaf. Greedy search takes the highest-probability child at every node and cannot recover from one early mistake; beam search keeps K plausible paths, classifies every frontier in parallel, and lets deeper evidence repair an ambiguous early decision.

The cookbook argues that decomposing classification into a hierarchy buys observability (which nodes your misclassifications occur in; how often each node and edge is traversed) and testability (unit-test hierarchy updates and measure their impact).

Inputs / state shape

The state is the plain document string. The hierarchy is a nested dict[str, Tree] of direct-child menus, loaded from four pinned sources:

Hierarchy Version Source
CPC patents 2026.05 CPCSchemeXML202605.zip (cooperativepatentclassification.org)
Shopify products 2026-02 Shopify/product-taxonomy v2026-02 dist/en/categories.txt
MeSH biomedical subjects 2026 desc2026.zip (nlmpubs.nlm.nih.gov)
CookSafe files snapshot 2026-08-06 codebase_files.txt (frozen repo listing)

MeSH is a DAG — one descriptor can appear under multiple parents — so the demo expands its official tree-number paths. The CookSafe listing is a frozen snapshot rather than a live walk, because line order is significant: sibling options are asked in the order they appear, so it is part of the question, not presentation.

The four documents and their expected leaves, verbatim:

Hierarchy document expected_leaf
CPC patents Patent abstract: a freestanding structural wooden perch for poultry or pet birds. The elevated roost has crossbars sized for bird feet and mounts inside an aviary. A01K31/12 Perches for poultry or birds, e.g. roosts
Shopify products Furniture listing: a wall-mounted window shelf bed. This padded floating shelf uses suction cups and a washable cushion as a sunny perch for one cat. Cat Window Beds & Perches
MeSH Clinical abstract: Crohn disease with transmural ileocolonic inflammation, skip lesions, abdominal pain, and chronic diarrhea. Colonoscopy showed cobblestoning and biopsy found noncaseating granulomas; treatment with infliximab produced remission. C06.405.469.432.500 Crohn Disease
CookSafe files Developer search: find the experimental Python module under x/eugene that implements BM25, dense, and fused retrievers for legal RAG. retrievers.py

Questions asked

Exactly one question type, asked once per sibling set. Verbatim:

def child_question(labels: tuple[str, ...]) -> tuple[Choice, dict[str, str]]:
    """Build the direct-child Choice and its reversible option mapping."""
    keys = {f"c{i}": label for i, label in enumerate(labels)}
    question = Choice(
        instructions="Which direct child category best matches this document?",
        criteria=keys,
    )
    return question, keys

The same instructions string is reused at every depth; all the node-specific information lives in criteria.

Combining logic in code

import os
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path

from typesafe_sdk import Choice, RetryPolicy, TypeSafeClient

HIERARCHIES = load_hierarchies()
MODEL, BEAM_WIDTH, MAX_DEPTH, EPSILON = "jev-1.12", 3, 12, 1e-9
client = TypeSafeClient(
    api_key=os.environ["TYPESAFE_API_KEY"],
    retry=RetryPolicy(max_retries=5, backoff_initial=1.0, backoff_max=20.0),
)


def choose(state: str, labels: tuple[str, ...]) -> dict[str, float]:
    """Ask one atomic direct-child question and return its distribution."""
    if len(labels) == 1:
        return {labels[0]: 1.0}
    question, keys = child_question(labels)
    response = client.system_one(
        state=state, questions={"child": question}, model=MODEL
    )
    probabilities = response.answers["child"].probabilities
    return {label: probabilities[key] for key, label in keys.items()}


def extend_candidate(candidate: dict, label: str, probabilities: dict[str, float]) -> dict:
    """Append one edge and recompute its geometric-mean path score."""
    is_decision: bool = len(probabilities) > 1
    probability_product: float = candidate["probability_product"] * (
        max(probabilities[label], EPSILON) if is_decision else 1.0
    )
    decision_count: int = candidate["decision_count"] + is_decision
    return {
        "path": candidate["path"] + (label,),
        "probability_product": probability_product,
        "decision_count": decision_count,
        "score": probability_product ** (1 / decision_count) if decision_count else 1.0,
    }

The beam itself, trimmed of diagram bookkeeping:

def beam_search(hierarchy) -> dict:
    """Parallel width-three beam search using geometric-mean probability."""
    beam = [{"path": (), "probability_product": 1.0, "decision_count": 0, "score": 1.0}]
    for _ in range(MAX_DEPTH):
        expandable = [c for c in beam if subtree(hierarchy.tree, c["path"])]
        finished = [c for c in beam if not subtree(hierarchy.tree, c["path"])]
        if not expandable:
            break
        with ThreadPoolExecutor(max_workers=BEAM_WIDTH) as executor:
            distributions = list(
                executor.map(
                    lambda c: choose(
                        hierarchy.document, tuple(subtree(hierarchy.tree, c["path"]))
                    ),
                    expandable,
                )
            )
        expanded = []
        for candidate, probabilities in zip(expandable, distributions, strict=True):
            for label in probabilities:
                expanded.append(extend_candidate(candidate, label, probabilities))
        beam = sorted(finished + expanded, key=lambda c: c["score"], reverse=True)[:BEAM_WIDTH]
    return {"beam": sorted(beam, key=lambda c: c["score"], reverse=True)}

Greedy is the same walk with label = max(probabilities, key=probabilities.get) and no beam.

Metrics.

Note that a sibling set of size 1 is not a decision: choose short-circuits to {label: 1.0} with no API call, and decision_count is not incremented.

Results / what the cookbook reports

With BEAM_WIDTH = 3, MAX_DEPTH = 12, model jev-1.12: beam search matched 4 of 4 expected leaves; greedy search matched 2 of 4. Keeping three paths recovered the expected classification for CPC patents and Shopify products.

Hierarchy Expected leaf Greedy leaf Beam K=3 leaf Greedy correct Beam correct
CPC patents A01K31/12 Perches for poultry or birds, e.g. roosts E99Z99/00 Subject matter not otherwise provided for in this section A01K31/12 Perches for poultry or birds, e.g. roosts no yes
Shopify products Cat Window Beds & Perches Pet Chairs Cat Window Beds & Perches no yes
MeSH biomedical subjects C06.405.469.432.500 Crohn Disease C06.405.469.432.500 Crohn Disease C06.405.469.432.500 Crohn Disease yes yes
CookSafe files retrievers.py retrievers.py retrievers.py yes yes

Four examples is the whole evaluation — treat this as a demonstration, not a benchmark (inferred).

Adapting it to a new domain

  1. Produce a nested dict of direct-child menus for your taxonomy (filesystem, org chart, moderation policy, LLM skill catalog, ontology). Fix the sibling order and keep it fixed, since order is part of the question.
  2. Leave child_question alone unless your labels need more than a name — if a label is cryptic, put a description in criteria instead of the bare label.
  3. Tune BEAM_WIDTH (cost per level scales with it, latency barely does because frontiers run in parallel) and MAX_DEPTH to your deepest path.
  4. Log separation_ratio per document and route low-separation documents to review.

Gotchas

Related

Sources