Questions
Choose between noul, choice, and score, shape each one so Drex accepts it, name the ids, and batch questions into one call.
Every request to POST /v1/systemone carries one state and a map of questions. Each question has a type of noul, choice, or score. This guide helps you pick the type, write the JSON that Drex accepts, and fit many questions into one call. To read what comes back, see Reading answers.
Pick the type by the decision you make next
Start from the code that consumes the answer, not from the text you are asking about.
| You need | Type | Example |
|---|---|---|
| A yes or no, with a threshold you set | noul | Is the customer asking for a refund? |
| One label from a fixed set with no order | choice | Is this ticket about billing, a technical problem, or the account? |
| A position on an ordered scale | score | How upset is the customer, from calm to angry? |
If the labels have an order, use score and you get a fractional position on the scale. If the labels have no order, use choice. If there are only two outcomes, use noul and compare the probability with a threshold.
Shape a noul question
{
"type": "noul",
"instructions": "Is the customer asking for a refund?",
"criteria": {
"true": "The customer asks for money back",
"false": "The customer asks for anything else"
}
}Drex applies these rules:
instructionsis optional, but when present it must be a string.nulland JSON objects are rejected withmust be a string.criteriais optional. Only thetrueandfalsekeys are read. Each value is a string ornull. Other keys are ignored.- The question needs a non-empty
instructionsor at least one non-empty criterion. Otherwise Drex rejects it withnoul needs instructions or criteria.
The TypeSafe SDK helper noul() defaults instructions to null. Drex rejects that, so pass a string: noul("Is the customer asking for a refund?"). See Migrate from TypeSafe.
Shape a choice question
{
"type": "choice",
"instructions": "What is this support ticket about?",
"criteria": {
"billing": "Payments, invoices, refunds, or charges",
"technical": "Bugs, errors, or outages",
"account": "Login, profile, or permissions",
"other": null
}
}criteriais required and must be an object. A string or an array is rejected withmust be an object with options.- It needs at least one label. An empty object is rejected with
choice needs at least 1 option. - Each description is a string or
null. A number or an object is rejected withmust be a string or null.
The keys of criteria are the labels that come back in choice and probabilities. Keep them short and machine friendly, such as billing rather than Billing and payments.
Shape a score question
{
"type": "score",
"instructions": "How upset is the customer?",
"criteria": ["Calm", "Mildly annoyed", "Frustrated", "Angry"]
}criteriais required and must be an array of strings. Anything else is rejected withmust be an array of level labels.- The array index is the score.
Calmis 0 andAngryis 3. Order the levels from low to high. - The API accepts one level or more. An empty array is rejected with
score needs at least 1 level. The TypeSafe SDK requires at least two levels, and a one-level scale gives the model nothing to choose between, so use two or more.
Name question ids
The keys of questions become the keys of answers. An id must be non-empty. Drex rejects a blank id with question ids must be non-empty.
Name each id for the field it fills in your code, for example wants_refund, category, sentiment. An id like q1 forces every reader of the answer to look up what q1 asked.
Batch questions into one call
One request accepts up to 512 questions about the same state. Drex bills input tokens, and a batch sends the state once instead of once per question, so one call with several questions costs less than one call per question.
We measured this with a 27-word support ticket. Output is an example and varies slightly per call.
| Request | usage.input_tokens |
|---|---|
| 1 noul question | 54 |
| 4 questions (2 noul, 1 choice, 1 score) in one call | 127 |
| The same 4 questions as 4 separate calls (54 + 78 + 55 + 54) | 241 |
The batch used 47% fewer input tokens than the separate calls. The answers matched. wants_replacement was 0.9988 in both runs, the category probabilities were identical to three decimals, and the urgency score was 1.522 in the batch and 1.528 alone.
import { TypeSafeClient, choice, noul, score } from "@typesafe-ai/sdk";
const client = new TypeSafeClient({
apiKey: process.env.DREX_API_KEY,
baseURL: "https://console.nace.ai",
defaultModel: "drex-v1.0",
});
const { answers, usage } = await client.systemOne({
state:
"Hi, I ordered a standing desk on March 3 and it arrived with a cracked top. I'd like a replacement, not a refund. Order #48213.",
questions: {
wants_replacement: noul("Is the customer asking for a replacement?"),
category: choice("What is this ticket about?", {
damaged_item: "Item arrived broken or damaged",
wrong_item: "A different item was delivered",
refund: "Customer wants money back",
other: null,
}),
urgency: score("How urgent is this ticket?", ["Low", "Medium", "High"]),
has_order_id: noul("Does the message include an order number?"),
},
});
console.log(answers.category.choice, answers.urgency.score, usage.input_tokens);More than 512 questions is rejected with at most 512 questions allowed. The state must fit the model's state limit, and the state plus your longest question must fit its row limit: 131,072 and 139,264 tokens on drex-v1.5. See State.
Write criteria that move the answer
Descriptions on choice labels change where the probability goes. We asked how to moderate the comment "This is the last time I'm warning you. Stop posting here or else." with the labels allow, flag, and remove, first with null descriptions and then with short ones.
| Descriptions | allow | flag | remove | confidence | input_tokens |
|---|---|---|---|---|---|
All null | 0.18 | 0.48 | 0.34 | 0.22 | 35 |
remove is "Explicit threat of violence or doxxing", flag is "Possible intimidation, send to a human reviewer", allow is "No policy violation" | 0.33 | 0.52 | 0.15 | 0.27 | 57 |
The comment is a vague threat. Once remove was defined as an explicit threat, its probability fell from 0.34 to 0.15 and the mass moved to allow and flag. Write a description for any label whose boundary is not obvious from its name. Each description costs input tokens, so keep descriptions short and skip them for labels like other.
For noul, criteria.true and criteria.false say what counts as yes and what counts as no. We asked whether the review "Would be great if the settings page didn't take five seconds to open every time." is a bug report, with the same instructions each time:
criteria | noul | input_tokens |
|---|---|---|
| None | 0.3505 | 33 |
true: "Anything that does not work as expected, including slowness", false: "A request for a new feature" | 0.9696 | 53 |
true: "A crash, an error, or a broken feature", false: "Slowness, polish, or a wish" | 0.0167 | 54 |
Without criteria the review is borderline. Each set of criteria settles it in its own direction. Use criteria when your team's definition of yes differs from the everyday meaning of the question. A sentence in instructions also works. "Is this review a bug report? Treat slowness and performance complaints as bugs." returned 0.6552 at 43 input tokens. Criteria state both sides of the boundary, so they moved the answer further here.
Fix a 422
When a question is malformed, Drex returns HTTP 422 with error.type of invalid_request_error. error.issues lists every problem with a path into your request body. error.message repeats the first one as path: message.
{
"error": {
"type": "invalid_request_error",
"message": "questions.category.criteria: choice needs at least 1 option",
"issues": [
{ "path": "questions.category.criteria", "message": "choice needs at least 1 option" }
]
},
"request_id": "req_<32 hex characters>"
}These are the messages a question mistake produces. a stands for your question id.
| Mistake | path | message |
|---|---|---|
Unknown type | questions.a.type | must be "noul", "choice", or "score" |
| Question is not an object | questions.a | must be an object |
instructions is null or an object | questions.a.instructions | must be a string |
| noul with no instructions and no criteria | questions.a.instructions | noul needs instructions or criteria |
noul criteria is a string | questions.a.criteria | must be an object |
choice criteria is an array or string | questions.a.criteria | must be an object with options |
choice criteria is {} | questions.a.criteria | choice needs at least 1 option |
| choice description is a number | questions.a.criteria.<label> | must be a string or null |
score criteria is not an array | questions.a.criteria | must be an array of level labels |
score criteria is [] | questions.a.criteria | score needs at least 1 level |
| score level is not a string | questions.a.criteria.<index> | must be a string |
questions is {} | questions | must contain at least one question |
questions is not an object | questions | must be an object |
| More than 512 questions | questions | at most 512 questions allowed |
| Blank question id | questions | question ids must be non-empty |
model is not a drex-* name, for example jev-latest | model | unknown model "jev-latest"; use "drex-v1.0", "drex-v1.5" or "drex-latest" |
A 422 is not retryable. Fix the body and send it again. For the other statuses, see Errors and retries and the full error reference.