symfonic.agent.structured¶
structured ¶
Structured output — typed, schema-validated final answers.
symfonic-core has always been able to constrain the model's tool choice
(supports_forced_tool_choice / ForcedToolChoiceResolver), but until
now the framework offered no first-class way to make the agent's final
answer conform to a caller-supplied schema. Adopters hand-rolled a forced
"respond" tool or post-parsed free text.
This module closes that gap with a single, provider-agnostic extraction pass.
The agent runs its normal tool loop to completion; when the caller passes a
response_model to :meth:SymfonicAgent.run, the engine hands the final
conversation to :func:extract_structured_output, which binds the Pydantic
schema onto the chat model via LangChain's with_structured_output and
returns a validated instance.
Design notes¶
- Post-loop, not mid-loop. Extraction is a terminal pass over the already-produced conversation, so tools run exactly as they do without structured output. This mirrors how Strands Agents layers structured output on top of the agent loop and avoids fighting the react topology.
- Provider-neutral.
with_structured_outputdispatches to whatever the underlyingBaseChatModelsupports (tool-calling for Anthropic / Bedrock Converse / OpenAI-compatible gateways, JSON mode where offered). The framework does not author the schema payload — the model does. - Fail-loud. A provider whose chat model cannot do structured output, or
a validation failure, raises :class:
StructuredOutputErrorrather than silently returningNone. Callers opted in explicitly by passing a schema; a quietNonewould hide a real contract break.
StructuredOutputError ¶
Bases: SymfonicAgentError
Raised when a structured-output extraction cannot be satisfied.
Carries code="unprocessable" so the FastAPI layer maps it to a 422
rather than a generic 500 — the request was well-formed, but the model
could not produce a value matching the requested schema.
Source code in src/symfonic/agent/structured.py
extract_structured_output
async
¶
extract_structured_output(
chat_model: Any,
messages: list[BaseMessage],
response_model: type[BaseModel],
*,
instruction: str | None = None,
) -> BaseModel
Return a validated response_model instance from the conversation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
chat_model
|
Any
|
The resolved |
required |
messages
|
list[BaseMessage]
|
The accumulated conversation (LangChain messages) produced by the agent loop. Used verbatim as context for the extraction. |
required |
response_model
|
type[BaseModel]
|
A Pydantic |
required |
instruction
|
str | None
|
Optional override for the terminal instruction appended
to |
None
|
Raises:
| Type | Description |
|---|---|
StructuredOutputError
|
When the provider cannot do structured output, the extraction call fails, or the result fails schema validation. |
Source code in src/symfonic/agent/structured.py
supports_structured_output ¶
True when chat_model exposes a usable with_structured_output.
BaseChatModel declares the method but raises NotImplementedError
for models that do not support it. A best-effort hasattr check plus a
"not the base sentinel" guard is the cheapest reliable probe; the actual
call is still wrapped so a late NotImplementedError degrades to a
clean :class:StructuredOutputError.