diff --git a/python/01-learn/14-structured-output/README.md b/python/01-learn/14-structured-output/README.md new file mode 100644 index 00000000..e724a2a1 --- /dev/null +++ b/python/01-learn/14-structured-output/README.md @@ -0,0 +1,98 @@ +# Structured Output with Strands Agents + +This tutorial teaches you how to get reliable, typed, validated data from your Strands agents using structured output. Instead of parsing free-form text, you define a Pydantic model and the agent returns a validated Python object — ready for downstream code, APIs, and workflows. + +## Architecture + +![Structured Output Architecture](images/architecture.png) + +The agent loop registers your Pydantic model as a dynamic tool. The LLM uses regular tools first, then calls the structured output tool last. Pydantic validates the output — if validation fails, the error is sent back to the LLM for self-correction. + +## Tutorial Details + +| Information | Details | +|------------------------|----------------------------------------------------------------------| +| **Strands Features** | Structured Output, Pydantic Validation, Streaming, Tool Integration | +| **Agent Pattern** | Single agent with structured output | +| **Tools** | `calculator` (from strands-agents-tools) | +| **Model** | Claude Sonnet 4.5 on Amazon Bedrock | + +## How It Works + +1. Developer defines a Pydantic `BaseModel` describing the desired output schema +2. The model is passed to the agent via `structured_output_model=MyModel` +3. The SDK converts the Pydantic model into a tool specification and registers it as a dynamic tool +4. The LLM processes the prompt, optionally using regular tools to gather information +5. The LLM calls the structured output tool with data matching the schema +6. Pydantic validates the output — on failure, the error is sent back and the LLM self-corrects +7. On success, the validated object is available at `result.structured_output` + +## Prerequisites + +- Python 3.10 or later +- AWS account with [Amazon Bedrock](https://aws.amazon.com/bedrock/) model access configured +- [Model access](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access-modify.html) enabled for Claude Sonnet 4.5 +- Basic understanding of Python and [Pydantic](https://docs.pydantic.dev/) +- Familiarity with Strands Agents basics [(see Tutorial 01)](../01-first-agent/) + +## Tutorial Structure + +``` +14-structured-output/ +├── README.md +├── requirements.txt +├── structured-output.ipynb +└── images/ + └── architecture.png +``` + +| File | Description | +|------|-------------| +| [structured-output.ipynb](./structured-output.ipynb) | Interactive notebook covering all 5 structured output patterns | + +## What You'll Learn + +- **Your First Structured Output**: Define a flat Pydantic model, pass it to an agent, and access typed results — both sync and async +- **Complex Schemas**: Build nested models with `List`, `Optional`, field validators, and enum constraints +- **Validation & Self-Correction**: Understand the automatic retry loop when validation fails, handle `StructuredOutputException`, and customize the forcing prompt +- **Tools + Structured Output**: Combine regular tools with structured output so agents can gather data and return structured results +- **Streaming Behavior**: Use `stream_async()` and understand that structured output appears only in the final event + +## Installation + +Install the required dependencies: + +```bash +pip install -r requirements.txt +``` + +## Running the Examples + +1. Open the notebook: [structured-output.ipynb](./structured-output.ipynb) +2. Run cells sequentially — each section builds on the previous one +3. Observe the agent's structured output in each example + +> **Note:** The exact field values will vary between runs since the LLM generates content dynamically. The structure and types will always match your Pydantic model. + +## Key Concepts + +- **Structured Output**: A feature that makes agents return validated Pydantic objects instead of free-form text +- **`structured_output_model`**: The parameter (on Agent constructor or per-call) that specifies the Pydantic model to use +- **Structured Output Tool**: A dynamic tool the SDK auto-registers from your Pydantic model — the LLM calls it to produce structured data +- **Validation & Self-Correction**: When the LLM's output fails Pydantic validation, the SDK sends the error back and the LLM retries automatically +- **Forcing**: If the LLM ignores the structured output tool, the SDK forces it by restricting available tools and setting `tool_choice` +- **`StructuredOutputException`**: Raised when the LLM fails to produce valid structured output even after forcing +- **`structured_output_prompt`**: A customizable message sent to the LLM when forcing is triggered + +## Additional Resources + +- [Strands Agents Documentation](https://strandsagents.com/) +- [Structured Output User Guide](https://strandsagents.com/latest/user-guide/concepts/agents/structured-output/) +- [Pydantic Documentation](https://docs.pydantic.dev/) +- [Strands Tools Repository](https://github.com/strands-agents/tools) + +## Next Steps + +- Learn about [Memory](../06-memory/) to persist agent memory across sessions +- Explore [Agents as Tools](../10-agents-as-tools/) to compose structured output agents into larger systems +- Try [Graph Workflows](../12-graph/) to build multi-step pipelines with structured data flowing between nodes diff --git a/python/01-learn/14-structured-output/images/architecture.png b/python/01-learn/14-structured-output/images/architecture.png new file mode 100644 index 00000000..df4c4c13 Binary files /dev/null and b/python/01-learn/14-structured-output/images/architecture.png differ diff --git a/python/01-learn/14-structured-output/requirements.txt b/python/01-learn/14-structured-output/requirements.txt new file mode 100644 index 00000000..155611e7 --- /dev/null +++ b/python/01-learn/14-structured-output/requirements.txt @@ -0,0 +1,3 @@ +strands-agents +strands-agents-tools +pydantic diff --git a/python/01-learn/14-structured-output/structured-output.ipynb b/python/01-learn/14-structured-output/structured-output.ipynb new file mode 100644 index 00000000..118b8539 --- /dev/null +++ b/python/01-learn/14-structured-output/structured-output.ipynb @@ -0,0 +1,632 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Structured Output with Strands Agents\n", + "\n", + "## Overview\n", + "\n", + "When you call a Strands agent, it returns free-form text by default. That's fine for chatbots, but when the agent's output needs to feed into downstream code — an API response, a database write, a UI component — you need structured, typed, validated data.\n", + "\n", + "In this tutorial, we'll walk you through how to use structured output to get back validated Python objects instead of strings.\n", + "\n", + "
\n", + " \n", + "
\n", + "\n", + "| Feature | Description |\n", + "|---------|-------------|\n", + "| **Flat Pydantic Models** | Define a model, pass it to the agent, and access typed results |\n", + "| **Complex Schemas** | Nested models, lists, optional fields, validators, and enums |\n", + "| **Validation & Self-Correction** | Automatic retry when validation fails, exception handling, custom forcing prompt |\n", + "| **Tools + Structured Output** | Combine tool use with structured results |\n", + "| **Streaming** | Understand where structured output fits in a streaming workflow |" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup and prerequisites\n", + "\n", + "### Prerequisites\n", + "* Python 3.10+\n", + "* AWS account\n", + "* Anthropic Claude Sonnet 4.5 enabled on Amazon Bedrock, [guide](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access-modify.html)\n", + "* Familiarity with Strands Agents basics [(see Tutorial 01)](../01-first-agent/)\n", + "\n", + "Let's now install the required packages" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip install -r requirements.txt -q" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Importing dependency packages\n", + "\n", + "Now let's import the dependency packages" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from enum import Enum\n", + "from typing import List, Literal, Optional\n", + "\n", + "from pydantic import BaseModel, Field, field_validator\n", + "\n", + "from strands import Agent\n", + "from strands.types.exceptions import StructuredOutputException" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Model used throughout this tutorial\n", + "MODEL_ID = \"us.anthropic.claude-sonnet-4-5-20250929-v1:0\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Your First Structured Output\n", + "\n", + "Let's start with the simplest use case: you want the agent to return data in a specific shape instead of free-form text.\n", + "\n", + "**The pattern:**\n", + "1. Define a Pydantic `BaseModel` with the fields you need\n", + "2. Pass it as `structured_output_model` when calling the agent\n", + "3. Access the typed result via `result.structured_output`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Defining a Pydantic Model\n", + "\n", + "This is a plain Pydantic model — the same kind you'd use for an API schema or database record. The `Field(description=...)` helps the LLM understand what each field expects." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class MovieReview(BaseModel):\n", + " \"\"\"A structured movie review.\"\"\"\n", + "\n", + " title: str = Field(description=\"The movie title\")\n", + " rating: int = Field(description=\"Rating from 1 to 10\")\n", + " summary: str = Field(description=\"A brief one-sentence summary of the review\")\n", + " recommend: bool = Field(description=\"Whether you would recommend this movie\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Synchronous call\n", + "\n", + "Let's pass `structured_output_model` when calling the agent. The result is a validated `MovieReview` instance — not a string, not a dict.\n", + "\n", + "> **Note:** The exact field values in the output will vary between runs since the LLM generates content dynamically. The structure and types will always match your Pydantic model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "agent = Agent(model=MODEL_ID)\n", + "\n", + "result = agent(\"Review the movie Inception by Christopher Nolan\", structured_output_model=MovieReview)\n", + "\n", + "# Access the typed result\n", + "review = result.structured_output\n", + "print(f\"Type: {type(review).__name__}\")\n", + "print(f\"Title: {review.title}\")\n", + "print(f\"Rating: {review.rating}/10\")\n", + "print(f\"Summary: {review.summary}\")\n", + "print(f\"Recommend: {review.recommend}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Asynchronous call\n", + "\n", + "The same pattern works with `invoke_async()` for async workflows." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "async def get_review_async():\n", + " agent = Agent(model=MODEL_ID)\n", + " result = await agent.invoke_async(\n", + " \"Review the movie The Matrix\",\n", + " structured_output_model=MovieReview,\n", + " )\n", + " review = result.structured_output\n", + " print(f\"[Async] {review.title}: {review.rating}/10 — {review.summary}\")\n", + "\n", + "\n", + "await get_review_async()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Complex Schemas\n", + "\n", + "Real-world data isn't flat. Let's see how structured output handles the same Pydantic features you'd use in production: nested models, lists, optional fields, constrained values, and enums." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Nested models and lists\n", + "\n", + "Let's define sub-models and compose them. The SDK converts the full schema — including nested `$ref` types — into a tool specification the LLM can understand." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Address(BaseModel):\n", + " \"\"\"A physical address.\"\"\"\n", + "\n", + " street: str = Field(description=\"Street address\")\n", + " city: str = Field(description=\"City name\")\n", + " state: str = Field(description=\"State or province\")\n", + " country: str = Field(description=\"Country\")\n", + "\n", + "\n", + "class Skill(BaseModel):\n", + " \"\"\"A professional skill with proficiency level.\"\"\"\n", + "\n", + " name: str = Field(description=\"Skill name\")\n", + " years_experience: int = Field(description=\"Years of experience\", ge=0)\n", + "\n", + "\n", + "class Candidate(BaseModel):\n", + " \"\"\"A job candidate profile.\"\"\"\n", + "\n", + " name: str = Field(description=\"Full name\")\n", + " address: Address = Field(description=\"Home address\")\n", + " skills: List[Skill] = Field(description=\"List of professional skills\")\n", + " bio: Optional[str] = Field(default=None, description=\"Short bio, if available\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "agent = Agent(model=MODEL_ID)\n", + "\n", + "result = agent(\n", + " \"Create a profile for a fictional senior software engineer based in Seattle with 3 skills\",\n", + " structured_output_model=Candidate,\n", + ")\n", + "\n", + "candidate = result.structured_output\n", + "print(f\"Name: {candidate.name}\")\n", + "print(f\"Location: {candidate.address.city}, {candidate.address.state}\")\n", + "print(f\"Bio: {candidate.bio}\")\n", + "print(\"Skills:\")\n", + "for skill in candidate.skills:\n", + " print(f\" - {skill.name}: {skill.years_experience} years\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Field constraints and enums\n", + "\n", + "We can use Pydantic's `Field` constraints (`ge`, `le`, `min_length`, etc.) and `Literal`/`Enum` types to restrict valid values. The LLM sees these constraints in the tool schema and respects them." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Priority(str, Enum):\n", + " \"\"\"Task priority levels.\"\"\"\n", + "\n", + " LOW = \"low\"\n", + " MEDIUM = \"medium\"\n", + " HIGH = \"high\"\n", + " CRITICAL = \"critical\"\n", + "\n", + "\n", + "class TicketAnalysis(BaseModel):\n", + " \"\"\"Analysis of a customer support ticket.\"\"\"\n", + "\n", + " category: str = Field(description=\"Issue category\", min_length=1)\n", + " sentiment: Literal[\"positive\", \"negative\", \"neutral\"] = Field(description=\"Customer sentiment\")\n", + " priority: Priority = Field(description=\"Ticket priority\")\n", + " confidence: float = Field(description=\"Confidence score\", ge=0.0, le=1.0)\n", + " summary: str = Field(description=\"Brief summary of the issue\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "agent = Agent(model=MODEL_ID)\n", + "\n", + "ticket_text = \"\"\"\n", + "I've been trying to reset my password for 3 days now and the reset email never arrives.\n", + "I've checked spam. This is blocking my entire team from accessing the dashboard.\n", + "We're paying for the enterprise plan and this level of service is unacceptable.\n", + "\"\"\"\n", + "\n", + "result = agent(\n", + " f\"Analyze this support ticket:\\n{ticket_text}\",\n", + " structured_output_model=TicketAnalysis,\n", + ")\n", + "\n", + "analysis = result.structured_output\n", + "print(f\"Category: {analysis.category}\")\n", + "print(f\"Sentiment: {analysis.sentiment}\")\n", + "print(f\"Priority: {analysis.priority.value}\")\n", + "print(f\"Confidence: {analysis.confidence:.0%}\")\n", + "print(f\"Summary: {analysis.summary}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Validation & Self-Correction\n", + "\n", + "What happens when the LLM returns data that doesn't pass Pydantic validation?\n", + "\n", + "The SDK handles this automatically:\n", + "1. The LLM calls the structured output tool with its data\n", + "2. Pydantic validates the data — if it fails, the SDK formats a per-field error message\n", + "3. The error is sent back to the LLM as a tool result\n", + "4. The LLM reads the error and self-corrects on the next attempt\n", + "\n", + "This happens within the normal agent loop — no extra code needed." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Triggering the retry loop\n", + "\n", + "Let's define a model with a strict `@field_validator` that the LLM is likely to violate on its first attempt. This lets us observe the self-correction cycle." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class StrictCode(BaseModel):\n", + " \"\"\"A product code that must follow a strict format.\"\"\"\n", + "\n", + " product_name: str = Field(description=\"Name of the product\")\n", + " product_code: str = Field(description=\"Product code in format: 3 uppercase letters, dash, 4 digits (e.g., ABC-1234)\")\n", + "\n", + " @field_validator(\"product_code\")\n", + " @classmethod\n", + " def validate_code_format(cls, v: str) -> str:\n", + " \"\"\"Enforce the exact format: AAA-0000.\"\"\"\n", + " import re\n", + "\n", + " if not re.match(r\"^[A-Z]{3}-\\d{4}$\", v):\n", + " raise ValueError(f\"Product code '{v}' must match format AAA-0000 (3 uppercase letters, dash, 4 digits)\")\n", + " return v" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "agent = Agent(model=MODEL_ID)\n", + "\n", + "# The LLM may initially produce a code like \"laptop-001\" — the validator will reject it,\n", + "# the error goes back to the LLM, and it self-corrects to something like \"LAP-0001\".\n", + "# You can enable debug logging (logging.getLogger(\"strands\").setLevel(logging.DEBUG))\n", + "# to see the validation error and retry in the agent's logs.\n", + "result = agent(\n", + " \"Generate a product entry for a wireless keyboard\",\n", + " structured_output_model=StrictCode,\n", + ")\n", + "\n", + "product = result.structured_output\n", + "print(f\"Product: {product.product_name}\")\n", + "print(f\"Code: {product.product_code}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Handling StructuredOutputException\n", + "\n", + "If the LLM fails to produce valid output even after the SDK forces it, a `StructuredOutputException` is raised. Always wrap structured output calls in a try/except for production code." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "agent = Agent(model=MODEL_ID)\n", + "\n", + "try:\n", + " result = agent(\n", + " \"Generate a product entry for a USB hub\",\n", + " structured_output_model=StrictCode,\n", + " )\n", + " product = result.structured_output\n", + " print(f\"Success: {product.product_name} — {product.product_code}\")\n", + "except StructuredOutputException as e:\n", + " print(f\"Structured output failed: {e}\")\n", + " print(\"Fallback: use the raw text response or retry with a different prompt\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Custom forcing prompt\n", + "\n", + "When the LLM doesn't call the structured output tool on its own, the SDK sends a forcing prompt to nudge it. The default is:\n", + "\n", + "> *\"You must format the previous response as structured output.\"*\n", + "\n", + "You can customize this with `structured_output_prompt` to give the LLM more specific guidance for your schema." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "agent = Agent(model=MODEL_ID)\n", + "\n", + "result = agent(\n", + " \"Generate a product entry for a mechanical keyboard\",\n", + " structured_output_model=StrictCode,\n", + " structured_output_prompt=(\n", + " \"Format your response using the structured output tool. \"\n", + " \"The product_code MUST be exactly 3 uppercase letters, a dash, then 4 digits (e.g., MKB-0001).\"\n", + " ),\n", + ")\n", + "\n", + "product = result.structured_output\n", + "print(f\"Product: {product.product_name}\")\n", + "print(f\"Code: {product.product_code}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Combining Tools with Structured Output\n", + "\n", + "In practice, agents don't just format text — they use tools to gather information first, then structure the result. The structured output tool coexists with regular tools. The LLM uses regular tools to do work, then calls the structured output tool last to return the final answer.\n", + "\n", + "Let's use the `calculator` tool from `strands-agents-tools` so the agent can perform calculations before returning a structured result." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from strands_tools import calculator\n", + "\n", + "\n", + "class InvestmentAnalysis(BaseModel):\n", + " \"\"\"Analysis of an investment calculation.\"\"\"\n", + "\n", + " initial_amount: float = Field(description=\"Initial investment amount in dollars\")\n", + " annual_rate: float = Field(description=\"Annual interest rate as a percentage\")\n", + " years: int = Field(description=\"Investment period in years\")\n", + " final_amount: float = Field(description=\"Calculated final amount after compound interest\")\n", + " total_return: float = Field(description=\"Total return as a percentage\")\n", + " recommendation: str = Field(description=\"Brief investment recommendation\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "agent = Agent(\n", + " model=MODEL_ID,\n", + " tools=[calculator],\n", + " system_prompt=\"You are a financial analyst. Use the calculator tool for all math operations.\",\n", + ")\n", + "\n", + "result = agent(\n", + " \"If I invest $10,000 at 7% annual compound interest for 15 years, what will I have? Analyze this investment.\",\n", + " structured_output_model=InvestmentAnalysis,\n", + ")\n", + "\n", + "analysis = result.structured_output\n", + "print(f\"Initial: ${analysis.initial_amount:,.2f}\")\n", + "print(f\"Rate: {analysis.annual_rate}%\")\n", + "print(f\"Period: {analysis.years} years\")\n", + "print(f\"Final: ${analysis.final_amount:,.2f}\")\n", + "print(f\"Return: {analysis.total_return:.1f}%\")\n", + "print(f\"Recommendation: {analysis.recommendation}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The agent used the `calculator` tool to compute compound interest, then called the structured output tool to return the result as a validated `InvestmentAnalysis` object. The tools and structured output work together seamlessly." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Streaming with Structured Output\n", + "\n", + "When building streaming UIs, you want to show progress while the agent works. With structured output:\n", + "\n", + "* **Text chunks may stream** as the agent reasons and uses tools\n", + "* **Structured output is NOT available incrementally** — it appears only in the **final event**\n", + "\n", + "This is because the structured output must be fully validated before it's returned.\n", + "\n", + "> **Note:** When structured output is active, the LLM primarily interacts via tool calls (the structured output tool). You may see minimal or no streaming text before the final structured result — this is expected behavior." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class BookSummary(BaseModel):\n", + " \"\"\"A structured book summary.\"\"\"\n", + "\n", + " title: str = Field(description=\"Book title\")\n", + " author: str = Field(description=\"Author name\")\n", + " genre: str = Field(description=\"Primary genre\")\n", + " themes: List[str] = Field(description=\"Key themes\")\n", + " one_liner: str = Field(description=\"One-sentence summary\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "async def stream_structured_output():\n", + " agent = Agent(model=MODEL_ID)\n", + "\n", + " print(\"Streaming events:\")\n", + " print(\"-\" * 40)\n", + "\n", + " async for event in agent.stream_async(\n", + " \"Summarize the book 'The Hitchhiker's Guide to the Galaxy' by Douglas Adams\",\n", + " structured_output_model=BookSummary,\n", + " ):\n", + " # Text chunks arrive during processing\n", + " if \"data\" in event:\n", + " print(f\"[stream] {event['data']}\", end=\"\", flush=True)\n", + "\n", + " # Structured output arrives only in the final result\n", + " if \"result\" in event:\n", + " print(\"\\n\" + \"-\" * 40)\n", + " print(\"Final structured output:\")\n", + " summary = event[\"result\"].structured_output\n", + " print(f\" Title: {summary.title}\")\n", + " print(f\" Author: {summary.author}\")\n", + " print(f\" Genre: {summary.genre}\")\n", + " print(f\" Themes: {', '.join(summary.themes)}\")\n", + " print(f\" Summary: {summary.one_liner}\")\n", + "\n", + "\n", + "await stream_structured_output()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The validated `BookSummary` object only appears in the final `result` event. Keep this in mind when designing your UI — you can show a loading indicator for the structured data while streaming text progress." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Key Takeaways\n", + "\n", + "You've learned how to get reliable, typed data from Strands agents:\n", + "\n", + "1. **Basic structured output** — Define a Pydantic model, pass it to the agent, get a validated object back\n", + "2. **Complex schemas** — Nested models, lists, optional fields, field constraints, and enums all work\n", + "3. **Validation & self-correction** — The SDK automatically retries when validation fails, and you can catch `StructuredOutputException` as a fallback\n", + "4. **Tools + structured output** — Agents use tools to gather data, then return structured results\n", + "5. **Streaming** — Structured output appears in the final event, not incrementally\n", + "\n", + "### Tips for production use\n", + "\n", + "* Use `structured_output_model=` (not the deprecated `agent.structured_output()` method)\n", + "* Add `Field(description=...)` to help the LLM understand your schema\n", + "* Always wrap production calls in `try/except StructuredOutputException`\n", + "* Use `structured_output_prompt` to improve success rates for strict schemas\n", + "* Structured output works with both sync (`agent()`) and async (`invoke_async()`, `stream_async()`)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbformat_minor": 5, + "pygments_lexer": "ipython3", + "version": "3.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}