pip install "vegaml[structured]"from typing import Literal
from pydantic import BaseModel, Field
from vegaml.structured import decide_model
class Ticket(BaseModel):
team: Literal["billing", "technical", "sales"] = Field(
description="Which team handles this?")
churn: bool = Field(description="Does the customer threaten to leave?")
severity: int = Field(ge=1, le=3, description="How severe?",
json_schema_extra={"criteria": ["cosmetic", "degraded", "outage"]})
ticket, result = decide_model(v, "I was billed twice, refund me or I cancel.", Ticket)
ticket.team # "billing", validated
result["answers"]["team"] # probabilities, prediction set, abstain: still all thereHow fields map#
| python type | becomes | notes |
|---|---|---|
bool | a boolean question | the field description is the statement |
Literal["a", "b"] | a choice over those values | str Enums work the same way |
int | a score | needs ge and le; the levels span the range |
Criteria for a score come from json_schema_extra, or are numbered when the range speaks for itself. The field description is required, because it is the instruction the model reads: a field with no description is a question with no question in it.
What you keep and what you gain#
The instance comes through pydantic validation, so an answer outside the field's type is a ValidationError rather than a quiet corruption. The second return value is the decide() dictionary untouched, so going typed loses none of the calibrated detail.
ticket, result = decide_model(v, text, Ticket)
if any(result["answers"][q]["abstain"] for q in result["answers"]):
escalate(text, result)
else:
act(ticket) # a real object, with real typesNote
Per-option definitions are worth more than a tidy schema.
A Literal gives the model bare option names with no definitions, and definitions are where most of the accuracy lives. If a choice is doing real work, write it as a plain question sheet with one-line criteria per option (see Questions and answers) and keep pydantic for the fields whose names already say what they mean.