Repository navigation
Support syntactic sugar for simplified questions/response modeling #5
Description
Activity
- changed the title
[-]Support syntactic sugar for simplified response models[/-][+]Support syntactic sugar for simplified questions/response modeling[/+]on Sep 18, 2026 This unified question-and-response modeling design would be a huge developer ergonomics win.
Here is a concrete schema-extraction and execution architecture for this proposal:
1. Schema & Question Extraction Mechanism
We can implement an
extract_questions_from_model(model_cls: type[BaseModel]) -> dict[str, Question]helper:- Inspect
model_cls.model_fields.items(). - For each field, inspect the annotation using
typing.get_originandtyping.get_args:- Noul (Boolean / Probability): If the type is
boolorfloat(or Annotated withNoulConfig), constructNoul(instructions=config.instructions or field.description). - Choice (Categorical): If the inner type is
Literal[...], extract string options intocriteria={val: config.criteria.get(val) if config else None for val in args}. - Score (Ordinal/Numeric): If the type is annotated with
ScoreConfigor mapped toScoreAnswer, extractlevels,rubric, andinstructions.
- Noul (Boolean / Probability): If the type is
- Any
Field(description=...)can act as the default fallback forinstructionswhen an explicitQuestionConfigis omitted, enabling ultra-terse definitions:class QuickFilter(BaseModel): is_urgent: bool = Field(description="Does this require immediate action?") sentiment: Literal["positive", "neutral", "negative"] = Field(description="Overall sentiment of the message")
2. Client-Level Integration
Inside
client.system_one():- If
response_modelis provided andquestionsis omitted, automatically invokequestions = extract_questions_from_model(response_model). - During response decoding, unpack
answersto instantiate and return the strongly-typedresponse_modelinstance directly. - Keeps 100% backwards compatibility with explicit
questions={...}.
Happy to open a branch with the implementation and full unit test coverage on my fork (https://github.com/aoright/typesafe-sdk-python) for maintainers to review/merge.
- Inspect
Implemented this proposal on branch
feat/pydantic-response-modelhttps://github.com/aoright/typesafe-sdk-python/tree/feat/pydantic-response-model
(latest commit76ba65caoright@76ba65c).Summary of Implementation
- Config Metadata Models:
- Added
QuestionConfig(instructions=..., criteria=...)andScoreConfig(instructions=..., levels=..., criteria=...)(withChoiceConfigandNoulConfigsubclasses) exported at the package root. - Supports both string instructions and structured mappings/sequences as accepted by the wire format.
- Automatically falls back to
Field(description=...)when explicit instructions are omitted. - Handles list vs tuple inputs interchangeably in
ScoreConfiglevels and criteria.
- Schema & Question Inference (
questions_from_model):
- Categorical / Choice:
Literal["..."], string-basedEnumsubclasses, orChoiceAnswer(including custom subclasses) mapped toChoice. Supports sequence criteria["label1", "label2"]as well as mappings. - Yes/No & Probability:
bool(coerced at>= 0.5),float, orNoulAnswer(including custom subclasses) mapped directly toNoul. - Score / Rubric:
ScoreAnswer(including custom subclasses), orfloat/intwithScoreConfig(levels=...)orScore(criteria=...)mapped toScore. - Strict validation: rejects empty models,
RootModel, conflicting annotations, and out-of-bound or unknown values.
- Response Decoding & Ergonomics (
parse_model):
- Automatically rounds fractional expected scores (e.g.
1.7->2) when the target field isint, supporting both standard andstrict=Truemodels withoutValidationError. - Accurately instantiates custom subclasses of
ChoiceAnswer,NoulAnswer, andScoreAnswer. - Overloads updated in both sync and async clients: when
questionsis omitted, questions are derived directly fromresponse_model, the request is sent, and answers are unpacked into the validatedresponse_modelinstance. - Added
inputparameter alias forstate, and automatic JSON serialization forBaseModelinput instances. - Preserves 100% backward compatibility when explicit
questions={...}is passed.
- Verification:
- 710 tests passing (
uv run pytest). - 0 errors across codebase in typecheck (
uv run pyrefly check). - Clean linter check (
uv run ruff check).
Hey @danielgafni,
I put together an implementation for this in my fork to see how cleanly it fits with the existing SDK architecture.
Key Design:
- Model & Field Mapping:
Literal[...]/Enum->Choice(with criteria derived from Enum docstrings or choices).bool/float->Noul(probability/threshold evaluation).ScoreAnswer/ bounded integers ->Score(rubric levels extracted from docstrings/criteria).
- Metadata Annotations:
- Added
QuestionConfig,ChoiceConfig,ScoreConfig, andNoulConfigfor fine-grained rules, criteria, and instruction overrides when defaults need tweaking. - Standard
Field(description=...)automatically maps to the question instruction if no explicit config is passed.
- Added
- Invocation & Deserialization:
- Supports
client.system_one(state=..., response_model=Ticket). - Automatically builds the questions payload, calls
/v1/systemone, and unpacks answers directly into typed attributes on the instantiated Pydantic model (ticket.category,ticket.urgent, etc.). - Preserves full backwards compatibility with explicit
questions={...}dictionary calls.
- Supports
All 710 unit and typing tests pass cleanly against the latest
v0.7.2main branch. The branch is rebased and available here if you'd like to take a look:
https://github.com/aoright/typesafe-sdk-python/tree/feat/pydantic-response-modelLet me know if this aligns with what you had in mind, and I can open a PR whenever you're ready!
- Model & Field Mapping:
A follow up for this issue.
It would be nice if the SDK could hold both question and response definitions on a single Pydantic model. Since question keys are matching response keys anyway, we could derive most of the information needed to construct the questions from types and additional
Annotatedmetadata.This interface should be as configurable as the raw
questions=parameter, and understand typical output modes (e.g.boolorfloatshould be both mapped toNoul).For example, it could look something like (quick draft):