Drex

Ask questions

client.system_one(state, questions, **options)

ParameterTypeRequiredDescription
stateAnyYesText, a JSON object, or an array to evaluate. See State.
questionsMapping[str, Question]Yes1 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.
modelstrNoModel for this call. Default: the client's model.
imagesSequence[ImageInput]NoDrex Max only, so pass model="drex-max" with it: up to 8 images that every question sees, in order. See Images.
retryRetryPolicyNoReplaces the client's retry policy for this call.
timeoutfloat | httpx.TimeoutNoReplaces the client's timeout for this call.
extra_headersMapping[str, str]NoExtra headers for this call.
extra_bodyMapping[str, Any]NoExtra top-level body fields, merged over the body last, so they win.
response_modeltype[BaseModel]NoParse 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.

decide.py
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

TypeFieldsDescription
Noultype ("noul", set for you), instructions: str | None = None, criteria: NoulCriteria | None = NoneA yes/no question. Needs instructions, or a criteria that describes an outcome.
Choicetype ("choice", set for you), criteria: Mapping[str, str | None], instructions: str | None = NonePicks one label. criteria maps each label to a description, or None. At least one label.
Scoretype ("score", set for you), criteria: Sequence[str], instructions: str | None = NoneScores on an ordered rubric, one description per score from zero. At least one entry.
NoulCriteriatrue: str | None, false: str | None (both optional)A TypedDict describing the yes and no outcomes of a Noul.
NoulModel, ChoiceModel, ScoreModeltype ("noul", "choice", "score"), plus the same fieldsThe plain-dictionary forms, as TypedDicts. criteria is required for choice and score.

These names are type aliases, for annotating your own code:

AliasWhat it means
QuestionModelA question written as a plain dictionary: a NoulModel, ChoiceModel or ScoreModel.
QuestionOne question, in either form: a Noul, Choice or Score object, or a QuestionModel dictionary.
QuestionsThe 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:

MemberTypeDescription
answersdict[str, Answer]Every answer, keyed by question name.
noulsdict[str, NoulAnswer]The yes/no answers.
choicesdict[str, ChoiceAnswer]The choice answers.
scoresdict[str, ScoreAnswer]The score answers.
modelstrThe model that answered. An alias such as drex-latest resolves to the model it serves.
usageUsageinput_tokens (billed), output_tokens, and image_tokens, each int | None. Image tokens are already included in input tokens.
evaluation_time_msfloat | NoneHow long the model took, in milliseconds.
request_idstrThe x-request-id header (else the body's request_id). Quote it in support requests.
raw_http_responsehttpx.ResponseThe 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.

AnswerFields
NoulAnswertype ("noul"), noul: float: the probability of yes, from 0 to 1.
ChoiceAnswertype ("choice"), choice: str (one of your labels), confidence: float (0 to 1), probabilities: dict[str, float] keyed by label.
ScoreAnswertype ("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:

ticket.py
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 UploadedFile that client.documents.upload returned, or the WorkspaceFileSource for one. It is sent as a workspace_file reference 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:

ParameterTypeRequiredDescription
contentstr | PathLike | bytes | BinaryIO | UploadedFile | WorkspaceFileSourceYesA 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.

drex_max.py
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.

TypeFields
ListModelsResponsemodels: tuple[ModelMetadata, ...], plus request_id and raw_http_response.
ModelMetadataname (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.

On this page