diff --git a/docs/user-guide/concepts/agents/structured-output.md b/docs/user-guide/concepts/agents/structured-output.md index 967a0c8b..52524547 100644 --- a/docs/user-guide/concepts/agents/structured-output.md +++ b/docs/user-guide/concepts/agents/structured-output.md @@ -1,26 +1,19 @@ # Structured Output -Structured output enables you to get type-safe, validated responses from language models using Pydantic models. Instead of receiving raw text that you need to parse, you can define the exact structure you want and receive a validated Python object that matches your schema. This transforms unstructured LLM outputs into reliable, program-friendly data structures that integrate seamlessly with your application's type system and validation rules. +!!! New + We have revamped the devx for structured output and deprecated the `structured_output_async` and `structured_output` methods. The following guide details how to use structured output. -## What is Structured Output? +## Introduction -Structured output allows you to constrain language model responses to follow a specific schema defined by a [Pydantic](https://docs.pydantic.dev/latest/concepts/models/) model. This ensures responses are properly formatted, validated, and type-safe, eliminating the need for manual parsing of text responses. +Structured output enables you to get type-safe, validated responses from language models using [Pydantic](https://docs.pydantic.dev/latest/concepts/models/) models. Instead of receiving raw text that you need to parse, you can define the exact structure you want and receive a validated Python object that matches your schema. This transforms unstructured LLM outputs into reliable, program-friendly data structures that integrate seamlessly with your application's type system and validation rules. ```mermaid flowchart LR - A[Pydantic Model] --> B[Agent.structured_output] - - subgraph Process[" "] - direction TB - C[convert_pydantic_to_tool_spec] --> D[LLM Response] - end - - B --> Process - Process --> E[Validated Pydantic Model] + A[Pydantic Model] --> B[Agent Invocation] + B --> C[LLM] --> D[Validated Pydantic Model] + D --> E[AgentResult.structured_output] ``` -*The conversion to tool spec happens behind the scenes* - Key benefits: - **Type Safety**: Get typed Python objects instead of raw strings @@ -29,204 +22,291 @@ Key benefits: - **IDE Support**: IDE type hinting from LLM-generated responses - **Error Prevention**: Catch malformed responses early -## How It Works -The structured output system converts your Pydantic models into tool specifications that guide the language model to produce correctly formatted responses. Various model providers supported in Strands Agents sdk-python can work with these specifications, with some supporting Pydantic `BaseModel` directly. +## Basic Usage -Strands handles this through the [`Agent.structured_output()`](../../../api-reference/agent.md#strands.agent.agent.Agent.structured_output) method, which manages the conversion, validation, and response processing automatically. +Define an output structure using a Pydantic model. Then, assign the model to the `structured_output_model` parameter when invoking the [`agent`](../../../api-reference/agent.md#strands.agent.agent). Then, access the Structured Output from the [`AgentResult`](../../../api-reference/agent.md#strands.agent.agent_result). + +```python +from pydantic import BaseModel, Field +from strands import Agent + +# 1) Define the Pydantic model +class PersonInfo(BaseModel): + """Model that contains information about a Person""" + name: str = Field(description="Name of the person") + age: int = Field(description="Age of the person") + occupation: str = Field(description="Occupation of the person") -## Usage +# 2) Pass the model to the agent +agent = Agent() +result = agent( + "John Smith is a 30 year-old software engineer", + structured_output_model=PersonInfo +) + +# 3) Access the `structured_output` from the result +person_info: PersonInfo = result.structured_output +print(f"Name: {person_info.name}") # "John Smith" +print(f"Age: {person_info.age}") # 30 +print(f"Job: {person_info.occupation}") # "software engineer" +``` + +???+ tip "Async Support" + Structured Output is supported with async via the `invoke_async` method: + + ```python + import asyncio + agent = Agent() + result = asyncio.run( + agent.invoke_async( + "John Smith is a 30 year-old software engineer", + structured_output_model=PersonInfo + ) + ) + ``` -Define your desired output structure using Pydantic models: +## More Information + +### How It Works + +The structured output system converts your Pydantic models into tool specifications that guide the language model to produce correctly formatted responses. All of the model providers supported in Strands can work with Structured Output. + +Strands handles this by accepting the `structured_output_model` parameter in [`agent`](../../../api-reference/agent.md#strands.agent.agent) invocations, which manages the conversion, validation, and response processing automatically. The validated result is available in the `AgentResult.structured_output` field. + + +### Error Handling + +In the event there is an issue with parsing the structured output, Strands will throw a custom `StructuredOutputException` that can be caught and handled appropriately: ```python -from pydantic import BaseModel, Field -from typing import Optional, List +from pydantic import ValidationError +from strands.types.exceptions import StructuredOutputException -class WeatherForecast(BaseModel): - """Complete weather forecast information.""" - location: str = Field(description="The location for this forecast") - current_time: str = Field(description="Current time in HH:MM format") - current_weather: str = Field(description="Current weather conditions") - temperature: Optional[float] = Field(default=None, description="Temperature in Celsius") - forecast_days: List[str] = Field(default_factory=list, description="Multi-day forecast") +try: + result = agent(prompt, structured_output_model=MyModel) +except StructuredOutputException as e: + print(f"Structured output failed: {e}") ``` -Then use the `Agent.structured_output()` method as shown in the following sections. +### Migration from Legacy API + +!!! warning "Deprecated API" + The `Agent.structured_output()` and `Agent.structured_output_async()` methods are deprecated. Use the new `structured_output_model` parameter approach instead. -> #### Note on Usage -> For model providers that do not provide a native "structured output" API, using `structured_ouput` creates a tool with the same name as the Pydantic model you have defined, say - "SomePydanticModel". -> `agent.structured_output` should be used to re-organize information from the existing conversation (or optional prompt) into the Pydantic model. To do this, `structured_output` expects to invoke only the SomePydanticModel tool. Attempting to use any other tool will result in errors. -> -> Note that the prompt attached to the `structured_output` call will not be integrated into the conversation history, therefore `structured_output` should solely be invoked to restructure data from a conversation or from the isolated optional prompt. +#### Before (Deprecated) -### Basic Usage +```python +# Old approach - deprecated +result = agent.structured_output(PersonInfo, "John is 30 years old") +print(result.name) # Direct access to model fields +``` -Extract structured information from text: +#### After (Recommended) ```python -from pydantic import BaseModel -from strands import Agent +# New approach - recommended +result = agent("John is 30 years old", structured_output_model=PersonInfo) +print(result.structured_output.name) # Access via structured_output field +``` -class PersonInfo(BaseModel): - name: str - age: int - occupation: str +### Best Practices -agent = Agent() -result = agent.structured_output( - PersonInfo, - "John Smith is a 30-year-old software engineer" -) +- **Keep models focused**: Define specific models for clear purposes +- **Use descriptive field names**: Include helpful descriptions with `Field` +- **Handle errors gracefully**: Implement proper error handling strategies with fallbacks + +### Related Documentation + +Refer to Pydantic documentation for details on: + +- [Models and schema definition](https://docs.pydantic.dev/latest/concepts/models/) +- [Field types and constraints](https://docs.pydantic.dev/latest/concepts/fields/) +- [Custom validators](https://docs.pydantic.dev/latest/concepts/validators/) + +## Cookbook + +### Auto Retries with Validation + +Automatically retry validation when initial extraction fails due to field validators: + +```python +from strands.agent import Agent +from pydantic import BaseModel, field_validator + + +class Name(BaseModel): + first_name: str -print(f"Name: {result.name}") # "John Smith" -print(f"Age: {result.age}") # 30 -print(f"Job: {result.occupation}") # "software engineer" + @field_validator("first_name") + @classmethod + def validate_first_name(cls, value: str) -> str: + if not value.endswith('abc'): + raise ValueError("You must append 'abc' to the end of my name") + return value + + +agent = Agent() +result = agent("What is Aaron's name?", structured_output_model=Name) ``` -### Multi-Modal Input +### Streaming Structured Output -Extract structured information from prompts containing images, documents, and other content types: +Stream structured output progressively while maintaining type safety and validation: ```python -class PersonInfo(BaseModel): - name: str - age: int - occupation: str +from strands import Agent +from pydantic import BaseModel, Field -with open("path/to/document.pdf", "rb") as fp: - document_bytes = fp.read() +class WeatherForecast(BaseModel): + """Weather forecast data.""" + location: str + temperature: int + condition: str + humidity: int + wind_speed: int + forecast_date: str + +streaming_agent = Agent() + +async for event in streaming_agent.stream_async( + "Generate a weather forecast for Seattle: 68°F, partly cloudy, 55% humidity, 8 mph winds, for tomorrow", + structured_output_model=WeatherForecast +): + if "data" in event: + print(event["data"], end="", flush=True) + elif "result" in event: + print(f'The forcast for today is: {event["result"].structured_output}') +``` -agent = Agent() -result = agent.structured_output( - PersonInfo, - [ - {"text": "Please process this application."}, - { - "document": { - "format": "pdf", - "name": "application", - "source": { - "bytes": document_bytes, - }, - }, - }, - ] +### Combining with Tools + +Combine structured output with tool usage to format tool execution results: + +```python +from strands import Agent +from strands_tools import calculator +from pydantic import BaseModel, Field + +class MathResult(BaseModel): + operation: str = Field(description="the performed operation") + result: int = Field(description="the result of the operation") + +tool_agent = Agent( + tools=[calculator] ) +res = tool_agent("What is 42 + 8", structured_output_model=MathResult) ``` -For a complete list of supported content types, please refer to the [API Reference](../../../api-reference/types.md#strands.types.content.ContentBlock). +### Multiple Output Types + +Reuse a single agent instance with different structured output models for varied extraction tasks: + +```python +from strands import Agent +from pydantic import BaseModel, Field +from typing import Optional + +class Person(BaseModel): + """A person's basic information""" + name: str = Field(description="Full name") + age: int = Field(description="Age in years", ge=0, le=150) + email: str = Field(description="Email address") + phone: Optional[str] = Field(description="Phone number", default=None) +class Task(BaseModel): + """A task or todo item""" + title: str = Field(description="Task title") + description: str = Field(description="Detailed description") + priority: str = Field(description="Priority level: low, medium, high") + completed: bool = Field(description="Whether task is completed", default=False) + + +agent = Agent() +person_res = agent("Extract person: John Doe, 35, john@test.com", structured_output_model=Person) +task_res = agent("Create task: Review code, high priority, completed", structured_output_model=Task) +``` ### Using Conversation History -Structured output can work with existing conversation context: +Extract structured information from prior conversation context without repeating questions: ```python +from strands import Agent +from pydantic import BaseModel +from typing import Optional + agent = Agent() # Build up conversation context agent("What do you know about Paris, France?") agent("Tell me about the weather there in spring.") -# Extract structured information with a prompt class CityInfo(BaseModel): city: str country: str population: Optional[int] = None climate: str -# Uses existing conversation context with a prompt -result = agent.structured_output(CityInfo, "Extract structured information about Paris") -``` - -### Complex Nested Models - -Handle sophisticated data structures: - -```python -from typing import List -from pydantic import BaseModel, Field - -class Address(BaseModel): - street: str - city: str - country: str - postal_code: Optional[str] = None - -class Contact(BaseModel): - email: Optional[str] = None - phone: Optional[str] = None - -class Person(BaseModel): - """Complete person information.""" - name: str = Field(description="Full name of the person") - age: int = Field(description="Age in years") - address: Address = Field(description="Home address") - contacts: List[Contact] = Field(default_factory=list, description="Contact methods") - skills: List[str] = Field(default_factory=list, description="Professional skills") - -agent = Agent() -result = agent.structured_output( - Person, - "Extract info: Jane Doe, a systems admin, 28, lives at 123 Main St, New York, NY. Email: jane@example.com" +# Extract structured information from the conversation +result = agent( + "Extract structured information about Paris from our conversation", + structured_output_model=CityInfo ) -print(result.name) # "Jane Doe" -print(result.address.city) # "New York" -print(result.contacts[0].email) # "jane@example.com" -print(result.skills) # ["systems admin"] +print(f"City: {result.structured_output.city}") # "Paris" +print(f"Country: {result.structured_output.country}") # "France" ``` -Refer to Pydantic documentation for details on: -- [Models and schema definition](https://docs.pydantic.dev/latest/concepts/models/) -- [Field types and constraints](https://docs.pydantic.dev/latest/concepts/fields/) -- [Custom validators](https://docs.pydantic.dev/latest/concepts/validators/) +### Agent-Level Defaults -### Error Handling +You can also aet a default structured output model that applies to all agent invocations: ```python -from pydantic import ValidationError +class PersonInfo(BaseModel): + name: str + age: int + occupation: str -try: - result = agent.structured_output(MyModel, prompt) -except ValidationError as e: - print(f"Validation failed: {e}") - # Handle appropriately - options include: - # 1. Retry with a more specific prompt - # 2. Fall back to a simpler model - # 3. Extract partial information from the error +# Set default structured output model for all invocations +agent = Agent(structured_output_model=PersonInfo) +result = agent("John Smith is a 30 year-old software engineer") + +print(f"Name: {result.structured_output.name}") # "John Smith" +print(f"Age: {result.structured_output.age}") # 30 +print(f"Job: {result.structured_output.occupation}") # "software engineer" ``` -### Async +!!! note "Note" + Since this is on the agent init level, not the invocation level, the expectation is that the agent will attempt structured output for each invocation. -Strands also supports obtaining structured output asynchronously through [`structured_output_async`](../../../api-reference/agent.md#strands.agent.agent.Agent.structured_output_async): -```python -import asyncio -from pydantic import BaseModel -from strands import Agent +### Overriding Agent Defaults +Even when you set a default `structured_output_model` at the agent initialization level, you can override it for specific invocations by passing a different `structured_output_model` during the agent invocation: + +```python class PersonInfo(BaseModel): name: str age: int occupation: str -async def structured_output(): - agent = Agent() - return await agent.structured_output_async( - PersonInfo, - "John Smith is a 30-year-old software engineer" - ) - -result = asyncio.run(structured_output()) -``` +class CompanyInfo(BaseModel): + name: str + industry: str + employees: int +# Agent with default PersonInfo model +agent = Agent(structured_output_model=PersonInfo) -## Best Practices +# Override with CompanyInfo for this specific call +result = agent( + "TechCorp is a software company with 500 employees", + structured_output_model=CompanyInfo +) -- **Keep models focused**: Define specific models for clear purposes -- **Use descriptive field names**: Include helpful descriptions with `Field` -- **Handle errors gracefully**: Implement proper error handling strategies with fallbacks -- **Extract key data at conversation completion**: Use structured output at the end of agent workflows to distill conversations into actionable data structures +print(f"Company: {result.structured_output.name}") # "TechCorp" +print(f"Industry: {result.structured_output.industry}") # "software" +print(f"Size: {result.structured_output.employees}") # 500 +```