Structured Output - AG2
Structured Output
Structured output constrains the model’s final message so you can parse it into a typed Python value—a number, a dataclass, a Pydantic model, or the result of your own validator—instead of treating the reply as an opaque string.
What you get on each turn
Every turn returns an AgentReply. Two surfaces matter for structured output:
| Surface | What it is |
|---|---|
reply.body |
Raw text from the model for that turn (a str or None). |
await reply.content() |
Parsed value according to the response schema in effect for that turn. |
If the model’s output cannot be parsed or fails validation, content() raises an error from the underlying parser (for example Pydantic’s validation errors). You can pass retries to automatically re-ask the model on failure.
With the default OpenAI client, when the schema exposes a JSON Schema to the API, the client sends a structured response_format so the model is guided to emit JSON matching that schema. PromptedSchema is the escape hatch when the provider does not support that mechanism: the schema is injected into the system prompt instead, and content() still runs the same way afterward.
When to use which tool
- Pass a plain type (
int,YourModel, …) when the default schema name and description are enough. - Use
ResponseSchemawhen you want a clearnameanddescriptionin the API payload so the model knows the role of the structured payload. - Use
@response_schemawhen you need custom parsing, normalization, or extra steps after JSON is read. - Use
PromptedSchemawhen your model or endpoint does not support native structured output.
Quick start
<br> 1<br> 2<br> 3<br> 4<br> 5<br> 6<br> 7<br> 8<br> 9<br>10<br>11<br>12<br>13<br>14<br> |
<br>from autogen.beta import Agent<br>from autogen.beta.config import OpenAIConfig<br>agent = Agent(<br> "assistant",<br> prompt="You are a helpful assistant. Answer concisely.",<br> config=OpenAIConfig("gpt-4o-mini"),<br> response_schema=int,<br>)<br>reply = await agent.ask("How many bits are in a byte?")<br>print(reply.body) # e.g. '8' — raw model text<br>result = await reply.content()<br>print(result) # 8 — Python int<br> |
Real-world examples
The following patterns mirror how structured output is used in applications: triage, extraction, and safe normalization.
Classify a support ticket (Pydantic)
Route incoming text into fields your helpdesk or CRM already understands:
<br> 1<br> 2<br> 3<br> 4<br> 5<br> 6<br> 7<br> 8<br> 9<br>10<br>11<br>12<br>13<br>14<br>15<br>16<br>17<br>18<br>19<br>20<br>21<br>22<br>23<br>24<br>25<br>26<br>27<br> |
<br>from typing import Annotated<br>from pydantic import BaseModel, Field<br>from autogen.beta import Agent<br>from autogen.beta.config import OpenAIConfig<br>class TicketTriage(BaseModel):<br> """Structured triage for a single support message."""<br> category: Annotated[str, Field(description="e.g. billing, bug, account_access")]<br> urgency: Annotated[str, Field(description="low, medium, or high")]<br> summary_one_line: Annotated[str, Field(description="Max 120 characters", max_length=120)]<br>agent = Agent(<br> "triage",<br> prompt="You triage customer support messages. Be conservative with urgency.",<br> config=OpenAIConfig("gpt-4o-mini"),<br> response_schema=TicketTriage,<br>)<br>body = (<br> "I was charged twice for Pro last week and I still can't export my reports. "<br> "This is blocking our quarter close."<br>)<br>reply = await agent.ask(f"Classify this ticket:\n\n{body}")<br>triage = await reply.content()<br># triage.category, triage.urgency, triage.summary_one_line → use in routing rules<br> |
Extract a delivery ETA window (dataclass)
Turn natural language into something your scheduling layer can consume:
<br> 1<br> 2<br> 3<br> 4<br> 5<br> 6<br> 7<br> 8<br> 9<br>10<br>11<br>12<br>13<br>14<br>15<br>16<br>17<br>18<br>19<br>20<br>21<br>22<br>23<br> |
<br>from dataclasses import dataclass<br>from autogen.beta import Agent<br>from autogen.beta.config import OpenAIConfig<br>@dataclass<br>class DeliveryWindow:<br> day_label: str<br> start_hour_local: int<br> end_hour_local: int<br> timezone: str<br>agent = Agent(<br> "scheduler",<br> prompt="Extract delivery windows as structured data only; use 24h integers for hours.",<br> config=OpenAIConfig("gpt-4o-mini"),<br> response_schema=DeliveryWindow,<br>)<br>reply = await agent.ask(<br> "Customer said: drop off Tuesday between 2 and 5pm Pacific, before dinner."<br>)<br>window = await reply.content()<br> |
Score a review on a fixed scale (primitive + clear prompt)
Use a primitive schema when the payload is a single JSON value and your prompt defines the scale:
<br> 1<br> 2<br> 3<br> 4<br> 5<br> 6<br> 7<br> 8<br> 9<br>10<br>11<br>12<br>13<br>14<br> |
<br>from autogen.beta import Agent<br>from autogen.beta.config import OpenAIConfig<br>agent = Agent(<br> "reviews",<br> prompt="You output a single integer 1–5 for overall satisfaction. No prose.",<br> config=OpenAIConfig("gpt-4o-mini"),<br> response_schema=int,<br>)<br>reply = await agent.ask(<br> "Rate this review: 'Shipped fast, packaging was torn, product works great.'"<br>)<br>stars = await reply.content()<br> |
Supported schema types
You can pass any type the stack can turn into a JSON Schema and parse back: primitives, dataclass, Pydantic models, unions, and more. Plain types are wrapped in an internal ResponseSchema instance for validation and API schema generation.
Primitives
<br>1<br>2<br>3<br>4<br>5<br> |
<br>agent = Agent("assistant", config=config, response_schema=int)<br>reply = await agent.ask("What is 2 + 2?")<br>result = await reply.content()<br># 4 — int<br> |
Dataclasses
<br> 1<br> 2<br> 3<br> 4<br> 5<br> 6<br> 7<br> 8<br> 9<br>10<br>11<br> |
<br>from dataclasses import dataclass<br>@dataclass<br>class City:<br> name: str<br> population: int<br>agent = Agent("assistant", config=config, response_schema=City)<br>reply = await agent.ask("Give the city name and approximate population for Kyoto.")<br>result = await reply.content()<br> |
Pydantic models
<br> 1<br> 2<br> 3<br> 4<br> 5<br> 6<br> 7<br> 8<br> 9<br>10<br> |
<br>from pydantic import BaseModel<br>class Sentiment(BaseModel):<br> label: str<br> score: float<br>agent = Agent("assistant", config=config, response_schema=Sentiment)<br>reply = await agent.ask("Analyze: 'I love this product!'")<br>result = await reply.content()<br> |
Unions
Use a union (int | str) or a tuple of types ((int, str)) when the model must return one of several JSON shapes.
<br> 1<br> 2<br> 3<br> 4<br> 5<br> 6<br> 7<br> 8<br> 9<br>10<br>11<br>12<br>13<br>14<br>15<br>16<br> |
``` from autogen.beta import Agent from autogen.beta.config import OpenAIConfig config = OpenAIConfig("gpt-4o-mini") # int |
ResponseSchema (named payloads)
For clearer API metadata, construct a ResponseSchema with an explicit name and description:
<br>1<br>2<br>3<br>4<br>5<br>6<br>7<br>8<br>9<br> |
``` from autogen.beta import Agent, ResponseSchema schema = ResponseSchema( int |
Those fields are attached to the structured-output payload where the provider supports it, which helps the model treat the JSON as a named contract rather than a generic blob.
Custom validation with @response_schema
Use the decorator when you need logic beyond “parse this JSON into a type”: clamping, regex cleanup, decoding wrapped JSON, or combining fields.
Sync validator: clamp a numeric rating
<br> 1<br> 2<br> 3<br> 4<br> 5<br> 6<br> 7<br> 8<br> 9<br>10<br>11<br> |
<br>from autogen.beta import Agent, response_schema<br>@response_schema<br>def parse_rating(content: str) -> int:<br> """Parse a rating and clamp it to 1–5."""<br> return max(1, min(5, int(content)))<br>agent = Agent("assistant", config=config, response_schema=parse_rating)<br>reply = await agent.ask("Rate this movie from 1 to 5.")<br>result = await reply.content()<br> |
Async validator: enrich after JSON parse
<br>1<br>2<br>3<br>4<br>5<br>6<br>7<br> |
<br>import json<br>@response_schema<br>async def fetch_and_validate(content: str) -> dict:<br> """Validate and enrich the model's JSON response."""<br> data = json.loads(content)<br> data["validated"] = True<br> return data<br> |
Validation rules for @response_schema
The framework introspects your function with fast_depents (the same dependency-injection path as @tool callables). Parameters satisfied by injection - Variables, Depends, Inject, Context and similar—are not part of the JSON the model must produce. Every other parameter controls how the completion text is decoded and whether a JSON Schema is attached for native structured output.
Accessing Context
Validators participate in the same dependency injection model as tools. Inject Context to read variables, tie validation to session state, or perform lookups:
<br>1<br>2<br>3<br>4<br>5<br>6<br>7<br> |
<br>from autogen.beta import Context, response_schema<br>@response_schema<br>def validate_with_context(content: str, context: Context) -> str:<br> """Use context variables during validation."""<br> language = context.variables.get("language", "en")<br> return f"[{language}] {content}"<br> |
PromptedSchema (models without native structured output)
Some models or providers do not support API-level structured output (no response_format JSON schema). PromptedSchema injects the JSON Schema into the system prompt and sets json_schema to None on the wire so the client does not request native structured mode. Validation still goes through the inner schema’s validate method.
Custom prompt template
The default template asks for raw JSON only. Override it with a string that contains the {schema} placeholder:
<br>1<br>2<br>3<br>4<br> |
<br>PromptedSchema(<br> int,<br> prompt_template="Reply with JSON matching this schema:\n{schema}",<br>)<br> |
Override schema per request
Pass response_schema to ask() (or AgentReply.ask()) to change the contract for one turn only. The agent’s default schema applies again on the next turn unless you override again:
<br>1<br>2<br>3<br>4<br> |
<br>agent = Agent("assistant", config=config)<br>turn = await agent.ask("How many seconds in a minute?", response_schema=int)<br>result = await turn.content()<br>#> 60 - int<br>turn2 = await turn.ask("Say hello.")<br>result2 = await turn2.content()<br>#> "Hello!" - str<br> |
Validation retries
When the model's response fails schema validation, you can automatically re-ask the model instead of raising immediately. Pass the retries keyword to content():
| Value | Behavior |
|---|---|
retries=0 (default) |
No retries — raise on the first validation failure. |
retries=3 |
Up to 3 re-asks after the initial attempt (4 total). |
retries=math.inf |
Re-ask indefinitely until the model produces a valid response. |
The retries parameter controls how many re-asks are allowed after the initial attempt. With retries=3, the initial response is validated; if it fails, the model is re-asked up to 3 more times before the error is raised.