Install
$ agentstack add skill-ag2ai-ag2-skills-ag2-structured-output ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Structured output
When to use
- The user wants a Pydantic model, dataclass, dict, primitive, or union back — not a string.
- They're doing classification, extraction, scoring, normalisation, or anything where downstream code parses the reply.
- They want automatic retry on validation failure.
60-second recipe
from pydantic import BaseModel, Field
from typing import Annotated
from ag2 import Agent
from ag2.config import OpenAIConfig
class TicketTriage(BaseModel):
category: Annotated[str, Field(description="e.g. billing, bug, account_access")]
urgency: Annotated[str, Field(description="low, medium, or high")]
summary_one_line: Annotated[str, Field(description="Max 120 characters", max_length=120)]
agent = Agent(
"triage",
prompt="You triage support messages. Be conservative with urgency.",
config=OpenAIConfig(model="gpt-4o-mini"),
response_schema=TicketTriage,
)
reply = await agent.ask("I was charged twice and can't export reports. Quarter close blocked.")
triage = await reply.content() # → typed TicketTriage
print(triage.category, triage.urgency)
reply.body is still the raw model text; await reply.content() runs validation and returns the parsed value. If validation fails, content() raises (e.g. pydantic.ValidationError).
Schema types you can pass
| Type | What you get | |---|---| | Primitive (int, float, bool) | Bare value, framework wraps in {"data": ...} for the API | | dataclass | Instance of the dataclass | | Pydantic BaseModel | Instance of the model | | Union (int \| str, (int, str)) | One of the alternatives | | dict[K, V], TypedDict | Validated dict | | ResponseSchema(...) | Same as above, with explicit name / description for the provider | | @response_schema callable | Custom validation/parsing logic | | PromptedSchema(inner) | Schema injected into the system prompt for providers without native structured output |
ResponseSchema — name your payload
Helps the provider treat the structured output as a named contract:
from ag2 import Agent, ResponseSchema
schema = ResponseSchema(int | str, name="ByteWidth", description="Number of bits in one byte.")
agent = Agent("assistant", config=config, response_schema=schema)
@response_schema — custom validation
For clamping, regex cleanup, decoding wrapped JSON, or combining fields:
from ag2 import Agent, response_schema
@response_schema
def parse_rating(content: str) -> int:
"""Parse a rating and clamp to 1–5."""
return max(1, min(5, int(content)))
agent = Agent("assistant", config=config, response_schema=parse_rating)
Multi-parameter form synthesises a JSON object schema from the parameter names:
from typing import Annotated
from pydantic import Field
from ag2 import response_schema
@response_schema
def extract_listing(
title: Annotated[str, Field(description="Product name")],
price_usd: Annotated[float, Field(description="Price in USD", ge=0)],
in_stock: Annotated[bool, Field(description="True if it ships now")],
) -> dict:
return {"title": title, "price_usd": price_usd, "in_stock": in_stock}
The function also participates in dependency injection — Context, Variable, Inject, Depends work the same way as in tools (and don't appear in the JSON schema).
Async validators are supported:
import json
@response_schema
async def fetch_and_validate(content: str) -> dict:
data = json.loads(content)
data["validated"] = True
return data
PromptedSchema — for providers without native structured output
Injects the JSON schema into the system prompt instead of using response_format:
from ag2 import Agent, PromptedSchema
agent = Agent("assistant", config=config, response_schema=PromptedSchema(int))
Wraps any inner schema (type, ResponseSchema, @response_schema callable). The validation logic stays the same; only the wire format changes.
Custom prompt template:
PromptedSchema(int, prompt_template="Reply with JSON matching this schema:\n{schema}")
Per-turn override
agent = Agent("assistant", config=config)
turn = await agent.ask("How many seconds in a minute?", response_schema=int)
print(await turn.content()) # 60
turn2 = await turn.ask("Say hello.") # back to default (no schema)
Pass response_schema=None to drop a schema set on the agent for one call.
Retries
When validation fails, automatically re-ask the model:
result = await reply.content(retries=3) # initial + up to 3 re-asks
result = await reply.content(retries=math.inf) # interactive only — could loop forever
The validation error is sent back to the model as a follow-up so it can correct itself.
Primitive embedding (embed)
Bare primitives (int, float, bool, list[T], primitive unions) get wrapped in {"data": ...} by default — most structured-output APIs handle objects more reliably than bare values. content() transparently unwraps. Opt out:
ResponseSchema(int, name="RawInt", embed=False) # model must produce a bare 42
@response_schema(embed=False)
def parse_rating(value: int) -> int: ...
Going deeper
- Working starter:
assets/recipe_builder.py(mirrorscode_examples/02) — Pydantic model +@tool+response_schema=. - Full reference:
website/docs/user-guide/structured_output.mdx— covers every schema type, multi-param@response_schema,Fieldconstraints,PromptedSchema, retries, embedding semantics.
Common pitfalls
- Reading
reply.bodywhen you wanted typed output —reply.bodyis the raw text.await reply.content()does the parsing. - Forgetting
awaitoncontent()— it's async; you'll get a coroutine, not the value. - No
descriptionin the Pydantic field — the LLM may guess what to put in each field. Add aField(description=...)for every non-obvious key. - Provider doesn't support native structured output — wrap with
PromptedSchema(...)rather than fighting the API. retries=math.infin production — will loop forever on a model that can't comply. Use a finite count.- Per-turn override is single-turn — passing
response_schema=intto oneask()doesn't change the agent's default. The next turn returns to whatever was set on the constructor.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: ag2ai
- Source: ag2ai/ag2-skills
- License: Apache-2.0
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.