Structured Output¶
Structured output makes the agent's final answer conform to a Pydantic schema you supply. The agent runs its normal tool loop, then a terminal pass validates the result against your model and returns a typed instance — no free-text parsing, no hand-rolled "respond" tool.
Quick start¶
from pydantic import BaseModel, Field
from symfonic.agent import SymfonicAgent, FrameworkConfig
class PersonInfo(BaseModel):
name: str = Field(description="Full name")
age: int = Field(description="Age in years")
agent = SymfonicAgent(config=FrameworkConfig(), model_provider=provider)
resp = await agent.run(
"John Smith is a 30-year-old software engineer.",
response_model=PersonInfo,
)
person = resp.structured # -> PersonInfo(name='John Smith', age=30)
print(person.name, person.age)
print(resp.final_response) # raw text answer is still present
Pass any Pydantic BaseModel subclass as response_model. The validated
instance lands on AgentResponse.structured; the model's prose answer stays
on AgentResponse.final_response, so you get both.
How it works¶
Structured output is a post-loop extraction pass, not a change to the react loop:
- The agent runs to completion exactly as it would without a schema — tools, memory hydration, metacognition, and consolidation all behave identically.
- The engine hands the final conversation to
symfonic.agent.structured.extract_structured_output, which binds your schema onto the chat model via LangChain'swith_structured_output. - The provider fills the schema (tool-calling on Anthropic / Bedrock Converse / OpenAI-compatible gateways, JSON mode where offered). The result is validated into a typed instance.
Because it runs after the loop, structured output composes with everything else — tools can run in the same call, and the schema never perturbs which tools the model sees mid-loop.
Works with any provider that can do structured output¶
response_model is provider-neutral. It works with the Anthropic, OpenAI,
Google, Bedrock (any model family), and OpenRouter providers — anything whose
underlying BaseChatModel implements with_structured_output. See
Model Providers.
Error handling — fail loud, not silent¶
You opted in by passing a schema, so a failure is surfaced, never swallowed:
from symfonic.agent import StructuredOutputError
try:
resp = await agent.run(query, response_model=PersonInfo)
except StructuredOutputError as exc:
# exc.code == "unprocessable" (maps to HTTP 422 in the FastAPI layer)
...
StructuredOutputError is raised when:
- the provider's chat model cannot do structured output, or
- the model's output fails schema validation.
When response_model is not passed, AgentResponse.structured is None
and behaviour is unchanged.
Guidance¶
- Describe every field.
Field(description=...)is the instruction the model reads. Vague field names produce vague extractions. - Keep schemas flat where you can. Deeply nested unions are harder for models to fill reliably; validate-and-retry at the application layer if you need strict guarantees.
- The prose answer is still there. Use
final_responsefor a human-readable message andstructuredfor the machine-readable payload in the same turn.
Related¶
- The Agent —
run()entry point andAgentResponse - Model Providers — provider capability matrix
- Capabilities — tools and forced tool-choice