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

Choice questions

[ concept ][ updated 2026-09-17 ][ confidence high ][ jev-1.13.0 ][ python sdk 0.6.0 ]#choice · primitives · classification · probabilities · confidence

TL;DR {"type": "choice", "instructions": "...", "criteria": {"option": "description or null", ...}}. The answer is {"type": "choice", "choice": "<top option>", "probabilities": {option: float summing to 1}, "confidence": 0..1}. Up to 255 options. Add an other option when your list may not cover every input; read probabilities and confidence, not just choice.

When to use / when not to use

Use a Choice when the answer is one of a fixed set of options: which team handles a ticket, which category a product belongs to, which language a code snippet is written in.

Example questions from raw/docs/primitives__choice.md:

"What programming language is this code written in"
  → options: python, javascript, typescript, go, rust, other

"What type of meeting is this based on the title and description"
  → options: standup, planning, retrospective, one on one, brainstorm, none of the above

"Which product category does this item belong to"
  → options: electronics, clothing, home garden, food and beverage

Request contract

The POST body to the TypeSafe API has three top-level fields: state (the content to evaluate), model, and questions (a map from question ids you choose to question objects).

Field Type Required Description
type "choice" yes Always "choice".
instructions string | object | array yes What the model should decide — the question it answers.
criteria map<string, string | null> yes The answer options. Each key is an option name; each value is a description of that option, or null when the option needs no extra detail.

Field types are as spelled in raw/docs/api.md. Per Structured instructions, options, levels, criteria, instructions and each criteria value may also be an object or an array (both are EntryType, which also permits null).

You choose the question id; the answer comes back under the same id, and the model never sees the id. The option names and their descriptions are both sent to the model, so write descriptions that separate the options from each other.

Minimal request

{
  "state": "My running shoes arrived in the wrong size. Can I swap them for a size 10?",
  "model": "jev-latest",
  "questions": {
    "department": {
      "type": "choice",
      "instructions": "Which team should handle this?",
      "criteria": {
        "returns": "Exchanges, refunds, wrong or damaged items",
        "shipping": "Delivery status, delays, lost packages",
        "billing": "Charges, invoices, payment problems"
      }
    }
  }
}

The same question in the Python SDK:

from typesafe_sdk import Choice, TypeSafeClient

with TypeSafeClient() as client:
    response = client.system_one(
        state="My running shoes arrived in the wrong size. Can I swap them for a size 10?",
        questions={
            "department": Choice(
                instructions="Which team should handle this?",
                criteria={
                    "returns": "Exchanges, refunds, wrong or damaged items",
                    "shipping": "Delivery status, delays, lost packages",
                    "billing": "Charges, invoices, payment problems",
                },
            ),
        },
    )

    print(response.answers["department"].choice)

Use the system_one method or the https://api.typesafe.ai/v1/systemone endpoint. The model field selects which model handles the request. In the JavaScript SDK the helper is choice(instructions, criteria), returning a ChoiceQuestion<T> — see JavaScript/TypeScript SDK: install, client, choice/score/noul.

Response contract

Field Type Description
type "choice" Matches the question type.
choice string The option with the highest probability.
probabilities map<string, number> Every option mapped to its probability. The values sum to 1.
confidence number 0 to 1, computed from how probabilities is spread. A flat shape (probability spread across several options) means low confidence; a single peak means high confidence.
{
  "model": "jev-latest",
  "answers": {
    "department": {
      "type": "choice",
      "choice": "returns",
      "confidence": 1.0,
      "probabilities": {
        "shipping": 0.0,
        "returns": 1.0,
        "billing": 0.0
      }
    }
  },
  "usage": {
    "input_tokens": 330,
    "output_tokens": 34
  }
}

This ticket is an easy one, so all of the probability is on returns and confidence is 1.0. A ticket that mentioned a wrong size and a missing refund would split probability between returns and billing, and confidence would drop. See Confidence vs probability.

Option limits and coverage

Ask more than one Choice per call

Ask every Choice question your code might need in a single request rather than one request per question. Questions are evaluated in parallel; adding questions barely changes the response time, and the code can ignore answers it doesn't need. Extra questions still cost tokens. See Primitives: Choice, Score, Noul and Speculative fan-out.

Worked example: five Choice questions, one ambiguous ticket

State: "Shoes arrived two weeks late and in the wrong size. Also I see two charges on my card. What are you going to do about this?"

{
  "state": "Shoes arrived two weeks late and in the wrong size. Also I see two charges on my card. What are you going to do about this?",
  "model": "jev-latest",
  "questions": {
    "department": {
      "type": "choice",
      "instructions": "Which team should handle this?",
      "criteria": {
        "returns": "Exchanges, refunds, wrong or damaged items",
        "shipping": "Delivery status, delays, lost packages",
        "billing": "Charges, invoices, payment problems"
      }
    },
    "return_reason": {
      "type": "choice",
      "instructions": "If the customer wants to return something, why?",
      "criteria": {
        "wrong_size": "The item doesn't fit",
        "wrong_item": "A different product was delivered",
        "damaged": "The item arrived broken or faulty",
        "changed_mind": "The item is fine, the customer no longer wants it",
        "other": "A return reason that fits none of the above"
      }
    },
    "shipping_issue": {
      "type": "choice",
      "instructions": "If this is a shipping problem, which kind is it?",
      "criteria": {
        "not_delivered": "The package never arrived",
        "delayed": "The package is late but still on its way",
        "wrong_address": "The package went to the wrong place",
        "damaged_in_transit": "The package arrived damaged",
        "other": "A shipping problem that fits none of the above"
      }
    },
    "requested_resolution": {
      "type": "choice",
      "instructions": "What does the customer want to happen?",
      "criteria": {
        "exchange": "Swap the item for a different one",
        "refund": "Money back",
        "replacement": "The same item sent again",
        "information": "Just an answer, no action needed"
      }
    },
    "tone": {
      "type": "choice",
      "instructions": "What is the customer's tone?",
      "criteria": {
        "calm": null,
        "frustrated": null,
        "angry": null
      }
    }
  }
}

Two of these are speculative: return_reason only matters if department is returns, and shipping_issue only if it's shipping. The tone question uses null descriptions because the option names are clear on their own.

Response:

{
  "model": "jev-latest",
  "answers": {
    "department": {
      "type": "choice",
      "choice": "returns",
      "confidence": 0.39,
      "probabilities": {
        "shipping": 0.02,
        "billing": 0.38,
        "returns": 0.6
      }
    },
    "return_reason": {
      "type": "choice",
      "choice": "wrong_size",
      "confidence": 1.0,
      "probabilities": {
        "wrong_size": 1.0,
        "wrong_item": 0.0,
        "other": 0.0,
        "changed_mind": 0.0,
        "damaged": 0.0
      }
    },
    "shipping_issue": {
      "type": "choice",
      "choice": "delayed",
      "confidence": 0.53,
      "probabilities": {
        "delayed": 0.63,
        "other": 0.37,
        "damaged_in_transit": 0.0,
        "not_delivered": 0.0,
        "wrong_address": 0.0
      }
    },
    "requested_resolution": {
      "type": "choice",
      "choice": "exchange",
      "confidence": 0.16,
      "probabilities": {
        "information": 0.1,
        "exchange": 0.37,
        "replacement": 0.24,
        "refund": 0.29
      }
    },
    "tone": {
      "type": "choice",
      "choice": "frustrated",
      "confidence": 0.88,
      "probabilities": {
        "angry": 0.08,
        "frustrated": 0.92,
        "calm": 0.0
      }
    }
  },
  "usage": {
    "input_tokens": 588,
    "output_tokens": 212
  }
}

How to read it:

Reading those answers in code

from typesafe_sdk import Choice, TypeSafeClient

TRIAGE_QUESTIONS = {
    "department": Choice(
        instructions="Which team should handle this?",
        criteria={
            "returns": "Exchanges, refunds, wrong or damaged items",
            "shipping": "Delivery status, delays, lost packages",
            "billing": "Charges, invoices, payment problems",
        },
    ),
    "return_reason": Choice(
        instructions="If the customer wants to return something, why?",
        criteria={
            "wrong_size": "The item doesn't fit",
            "wrong_item": "A different product was delivered",
            "damaged": "The item arrived broken or faulty",
            "changed_mind": "The item is fine, the customer no longer wants it",
            "other": "A return reason that fits none of the above",
        },
    ),
    "shipping_issue": Choice(
        instructions="If this is a shipping problem, which kind is it?",
        criteria={
            "not_delivered": "The package never arrived",
            "delayed": "The package is late but still on its way",
            "wrong_address": "The package went to the wrong place",
            "damaged_in_transit": "The package arrived damaged",
            "other": "A shipping problem that fits none of the above",
        },
    ),
    "requested_resolution": Choice(
        instructions="What does the customer want to happen?",
        criteria={
            "exchange": "Swap the item for a different one",
            "refund": "Money back",
            "replacement": "The same item sent again",
            "information": "Just an answer, no action needed",
        },
    ),
    "tone": Choice(
        instructions="What is the customer's tone?",
        criteria={"calm": None, "frustrated": None, "angry": None},
    ),
}


def triage(ticket: str) -> None:
    with TypeSafeClient() as client:
        response = client.system_one(
            state=ticket,
            questions=TRIAGE_QUESTIONS,
        )
    answers = response.answers

    department = answers["department"]
    if department.confidence < 0.3:
        # Not clear which team to send to. Let a person decide.
        send_to_manual_triage(ticket)
        return

    if department.choice == "returns":
        # return_reason answer is only used here
        assign(ticket, team="returns", issue=answers["return_reason"].choice)
    elif department.choice == "shipping":
        # shipping_issue answer is only used here
        assign(ticket, team="shipping", issue=answers["shipping_issue"].choice)
    else:
        assign(ticket, team="billing")

    # A second team with a real share of the probability gets a copy
    for team, probability in department.probabilities.items():
        if team != department.choice and probability > 0.25:
            notify(ticket, team=team)

    resolution = answers["requested_resolution"]
    if resolution.confidence < 0.5:
        # The customer hasn't said what they want. Ask, don't guess.
        ask_customer_what_they_want(ticket)
    elif resolution.choice == "refund":
        flag_for_refund_approval(ticket)

    if answers["tone"].choice == "angry":
        flag_for_senior_agent(ticket)

For the ticket above this assigns the ticket to the returns team with issue wrong_size, sends the billing team a copy, and asks the customer what they want. The code does not use the shipping_issue answer. One request, five answers, and the routing logic is ordinary if statements. Adding another Choice question keeps the request count at one.

The smart home assistant demo evaluates every user request against a long list of Choice questions in one call: request category, room, device, and action — most irrelevant to any one request, and the code ignores them.

Structured instructions and criteria

Start with a one-line description per option. When two options are similar and the model keeps confusing them, describe each one with an object instead of a string: what the option covers, what belongs to a neighboring option instead, and a few example inputs.

{
  "state": "I sent the shoes back a week ago. When do I get my money?",
  "model": "jev-latest",
  "questions": {
    "return_topic": {
      "type": "choice",
      "instructions": {
        "question": "Which returns topic is the customer asking about?",
        "focus": "Classify the information the customer wants."
      },
      "criteria": {
        "return_policy": {
          "what": "Whether and how an item can be returned",
          "not_for": "Progress of a return already sent",
          "examples": [
            "Can I return shoes I've worn once?",
            "How long do I have to return an order?"
          ]
        },
        "return_status": {
          "what": "Progress of a return already sent",
          "not_for": "Whether and how an item can be returned",
          "examples": [
            "Has my return arrived yet?",
            "When will my refund be paid?"
          ]
        }
      }
    }
  }
}
{
  "model": "jev-latest",
  "answers": {
    "return_topic": {
      "type": "choice",
      "choice": "return_status",
      "confidence": 1.0,
      "probabilities": {
        "return_policy": 0.0,
        "return_status": 1.0
      }
    }
  },
  "usage": {
    "input_tokens": 407,
    "output_tokens": 32
  }
}

The field names question, focus, what, not_for, and examples are not part of the API, and none are reserved. You choose them, the same way you choose option names. The model sees the names along with the values, so use short names that label what follows. More shapes — including taxonomy subtrees as option values — in Structured instructions, options, levels, criteria.

Gotchas

Related

Sources