Drex
Ask questions
client.system_one(state, questions, **options)
| Parameter | Type | Required | Description |
|---|---|---|---|
state | Any | Yes | Text, a JSON object, or an array to evaluate. See State. |
questions | Mapping[str, Question] | Yes | 1 to 512 questions, keyed by the name used to identify each answer. Each is a Noul, Choice or Score, or a plain dictionary. See Questions. |
model | str | No | Model for this call. Default: the client's model. |
images | Sequence[ImageInput] | No | Drex Max only, so pass model="drex-max" with it: up to 8 images that every question sees, in order. See Images. |
retry | RetryPolicy | No | Replaces the client's retry policy for this call. |
timeout | float | httpx.Timeout | No | Replaces the client's timeout for this call. |
extra_headers | Mapping[str, str] | No | Extra headers for this call. |
extra_body | Mapping[str, Any] | No | Extra top-level body fields, merged over the body last, so they win. |
response_model | type[BaseModel] | No | Parse the response into your own Pydantic model instead of SystemOneResponse. See Parse into your own model. |
It returns a SystemOneResponse. A decision is billed per input token and only when it returns 200, so the SDK's retries never bill twice.
from nace_sdk import Choice, NaceClient, Noul, Score
with NaceClient() as client:
result = client.system_one(
state="I was charged twice for my March invoice.",
questions={
"wants_refund": Noul(instructions="Is the customer asking for a refund?"),
"topic": Choice(instructions="Which topic is it?", criteria={"billing": None, "shipping": None, "other": None}),
"urgency": Score(instructions="How urgent is it?", criteria=["low", "medium", "high"]),
},
)
print(result.nouls["wants_refund"].noul)
print(result.choices["topic"].choice, result.choices["topic"].confidence)
print(result.scores["urgency"].score, result.usage.input_tokens, result.request_id)Question types
| Type | Fields | Description |
|---|---|---|
Noul | type ("noul", set for you), instructions: str | None = None, criteria: NoulCriteria | None = None | A yes/no question. Needs instructions, or a criteria that describes an outcome. |
Choice | type ("choice", set for you), criteria: Mapping[str, str | None], instructions: str | None = None | Picks one label. criteria maps each label to a description, or None. At least one label. |
Score | type ("score", set for you), criteria: Sequence[str], instructions: str | None = None | Scores on an ordered rubric, one description per score from zero. At least one entry. |
NoulCriteria | true: str | None, false: str | None (both optional) | A TypedDict describing the yes and no outcomes of a Noul. |
NoulModel, ChoiceModel, ScoreModel | type ("noul", "choice", "score"), plus the same fields | The plain-dictionary forms, as TypedDicts. criteria is required for choice and score. |
These names are type aliases, for annotating your own code:
| Alias | What it means |
|---|---|
QuestionModel | A question written as a plain dictionary: a NoulModel, ChoiceModel or ScoreModel. |
Question | One question, in either form: a Noul, Choice or Score object, or a QuestionModel dictionary. |
Questions | The whole questions argument: a dictionary from each question's name to a Question. |
questions = {
"wants_refund": {"type": "noul", "instructions": "Is the customer asking for a refund?"},
"is_spam": Noul(criteria={"true": "Unsolicited advertising", "false": "A real customer message"}),
}The SDK checks the questions before it sends anything and raises NaceError for no questions or more than 512, a Noul with neither instructions nor a described outcome, a Choice whose criteria isn't a nonempty mapping, a Score with no criteria, and a dictionary without a nonempty type (or, for choice and score, without criteria). An unknown field on a question object raises pydantic.ValidationError. Unset instructions and criteria are left out of the request.
Read the response
SystemOneResponse:
| Member | Type | Description |
|---|---|---|
answers | dict[str, Answer] | Every answer, keyed by question name. |
nouls | dict[str, NoulAnswer] | The yes/no answers. |
choices | dict[str, ChoiceAnswer] | The choice answers. |
scores | dict[str, ScoreAnswer] | The score answers. |
model | str | The model that answered. An alias such as drex-latest resolves to the model it serves. |
usage | Usage | input_tokens (billed), output_tokens, and image_tokens, each int | None. Image tokens are already included in input tokens. |
evaluation_time_ms | float | None | How long the model took, in milliseconds. |
request_id | str | The x-request-id header (else the body's request_id). Quote it in support requests. |
raw_http_response | httpx.Response | The HTTP response, for its status, headers and body. |
request_id and raw_http_response aren't fields, so model_dump() leaves them out. Every response type has them, and a from_http_response(response) class method that builds one from an httpx.Response you fetched yourself, raising the same errors as a call.
| Answer | Fields |
|---|---|
NoulAnswer | type ("noul"), noul: float: the probability of yes, from 0 to 1. |
ChoiceAnswer | type ("choice"), choice: str (one of your labels), confidence: float (0 to 1), probabilities: dict[str, float] keyed by label. |
ScoreAnswer | type ("score"), score: float (expected score, can fall between levels), confidence: float, legend: tuple[str, ...] (your rubric), probabilities: dict[int, float] keyed by integer score. |
Answer is the union of the three, told apart by type. Score probabilities are keyed by int, so read them as result.scores["urgency"].probabilities[2]. An answer of a type this SDK version doesn't know is dropped with a warning on the nace_sdk logger; it is still in raw_http_response. See Reading answers.
Parse into your own model
Subclass SystemOneResponse and declare an answer per question name. The SDK copies each declared answer from answers into its field (answers still has it too), and you keep request_id and raw_http_response:
from nace_sdk import Choice, ChoiceAnswer, NaceClient, Noul, NoulAnswer, SystemOneResponse
class Ticket(SystemOneResponse):
wants_refund: NoulAnswer
topic: ChoiceAnswer
with NaceClient() as client:
ticket = client.system_one(
state="I was charged twice for my March invoice.",
questions={
"wants_refund": Noul(instructions="Is the customer asking for a refund?"),
"topic": Choice(criteria={"billing": None, "shipping": None}),
},
response_model=Ticket,
)
print(ticket.wants_refund.noul, ticket.topic.choice, ticket.request_id)Any other pydantic.BaseModel is validated against the raw JSON body and has no request_id or raw_http_response. A body that doesn't match raises NaceAPIResponseValidationError.
Images
Drex Max (drex-max) is the only model that reads images. Pass them in images and refer to them as "Image 1", "Image 2" and so on in the state or the instructions; media inside state or questions returns 422. Each entry, an ImageInput, is one of:
- an
UploadedFilethatclient.documents.uploadreturned, or theWorkspaceFileSourcefor one. It is sent as aworkspace_filereference and doesn't count toward the 4 MiB request body, so use it for anything but small images; - a standard base64 string, or a data URL such as
data:image/jpeg;base64,...; - the wire's own
{"type": "workspace_file", "workspace_id": ..., "file_id": ...}dictionary.
ImageInput is str | UploadedFile | WorkspaceFileSource | Mapping[str, str]. Any other entry, a string that is neither base64 nor a data URL (such as a file path: pass image_input(path) instead), or a single string in place of the list, raises NaceError before sending. A file in another account's returns 422, and so does images with any model but drex-max. Images are PNG, JPEG or WebP, up to 8 per request, 16 MiB each and 32 MiB in all. Their tokens are in usage.image_tokens, already counted in input_tokens.
image_input(content), from nace_sdk:
| Parameter | Type | Required | Description |
|---|---|---|---|
content | str | PathLike | bytes | BinaryIO | UploadedFile | WorkspaceFileSource | Yes | A path, the image's bytes or an open binary file, returned as a data URL typed from its first bytes. An UploadedFile or WorkspaceFileSource returns its workspace_file dictionary. |
It returns a str | dict[str, str] for images. Content that isn't a PNG, JPEG or WebP image, or is over 16 MiB, raises NaceError. An inline image counts toward the 4 MiB request body, base64 expansion included.
from nace_sdk import NaceClient, Noul, image_input
with NaceClient() as client:
receipt = client.documents.upload("receipt.jpg")
result = client.system_one(
state="Image 1 is a scanned receipt. Image 2 is the shop's logo.",
questions={"same_shop": Noul(instructions="Was the receipt in Image 1 issued by the shop whose logo is Image 2?")},
model="drex-max",
images=[receipt, image_input("logo.png")],
)
print(result.nouls["same_shop"].noul, result.usage.image_tokens)List models
client.models.list(**options) takes retry, timeout and extra_headers, and returns a ListModelsResponse. It works at any balance and is free, so it doubles as a key check.
| Type | Fields |
|---|---|
ListModelsResponse | models: tuple[ModelMetadata, ...], plus request_id and raw_http_response. |
ModelMetadata | name (what model takes), description, release_date (YYYY-MM-DD), alias_for (the model an alias such as drex-latest serves; None for a pinned model). |
with NaceClient() as client:
print([model.name for model in client.models.list().models])See Models.