Skip to content

Structured Outputs with Amazon Bedrock in AG2#

Open In Colab Open on GitHub

This notebook demonstrates how to use structured outputs with Amazon Bedrock in AG2. Structured outputs allow you to define a specific JSON schema that the model must follow, ensuring consistent and parseable responses.

What are Structured Outputs?#

Structured outputs enable you to: - Define a schema: Specify exactly what format you want the model’s response in - Get consistent results: The model will always return data matching your schema - Parse easily: Responses are guaranteed to be valid JSON matching your structure - Validate automatically: Use Pydantic models to validate and type-check responses

How Bedrock Implements Structured Outputs#

Bedrock uses Tool Use (Function Calling) to implement structured outputs. When you provide a response_format, AG2:

  1. Creates a special tool with your schema as the input schema
  2. Forces the model to call this tool using toolChoice
  3. Extracts the structured data from the tool call
  4. Validates it against your Pydantic model or dict schema

This approach is based on the AWS Bedrock Converse API.

Requirements#

  • Python >= 3.10
  • AG2 installed: pip install ag2
  • boto3 package: pip install boto3
  • AWS credentials configured (via environment variables, IAM role, or AWS credentials file)
  • A Bedrock model that supports Tool Use (e.g., Claude models)

Model Compatibility#

Not all Bedrock models support Tool Use. Models that do support structured outputs include: - anthropic.claude-3-5-sonnet-20241022-v2:0 - anthropic.claude-3-sonnet-20240229-v1:0 - anthropic.claude-3-opus-20240229-v1:0 - anthropic.claude-3-haiku-20240307-v1:0

Check the Bedrock model documentation for the latest list of models supporting Tool Use.

Installation#

Install required packages if not already installed:

%pip install ag2 boto3 pydantic --upgrade

Setup: Import Libraries and Configure AWS Credentials#

import json
import os

from dotenv import load_dotenv
from pydantic import BaseModel

from autogen import ConversableAgent, LLMConfig

load_dotenv()

print("Libraries imported successfully!")

Part 1: Define Structured Output Models with Pydantic#

Pydantic models provide type safety and automatic validation. Let’s create a model for math problem solving:

# Define structured output model for math problem solving
class Step(BaseModel):
    """Represents a single step in solving a math problem."""

    explanation: str  # What operation or reasoning is being performed
    output: str  # The result of this step

class MathReasoning(BaseModel):
    """Complete structured response for a math problem solution."""

    steps: list[Step]  # List of all steps taken
    final_answer: str  # The final answer

    def format(self) -> str:
        """Format the structured output for human-readable display."""
        steps_output = "\n".join(
            f"Step {i + 1}: {step.explanation}\n  Output: {step.output}" for i, step in enumerate(self.steps)
        )
        return f"{steps_output}\n\nFinal Answer: {self.final_answer}"

print("Pydantic models defined:")
print(f"- Step: {Step.model_json_schema()}")
print(f"- MathReasoning: {MathReasoning.model_json_schema()}")

Part 2: Configure Bedrock with Structured Outputs#

Now let’s set up the LLM configuration with Bedrock and enable structured outputs:

# Configure LLM with Bedrock and structured outputs
llm_config = LLMConfig(
    config_list={
        "api_type": "bedrock",
        "model": "qwen.qwen3-coder-480b-a35b-v1:0",
        "api_key": os.getenv("BEDROCK_API_KEY"),
        "aws_region": os.getenv("AWS_REGION", "eu-north-1"),
        "aws_access_key": os.getenv("AWS_ACCESS_KEY"),
        "aws_secret_key": os.getenv("AWS_SECRET_ACCESS_KEY"),
        "response_format": MathReasoning,
        "aws_profile_name": os.getenv("AWS_PROFILE"),
    },
    cache_seed=42,  # Optional: for reproducible results
)
print("Bedrock LLM configuration created with structured outputs!")

Key Configuration Parameters#

  • api_type: "bedrock": Tells AG2 to use the Bedrock client
  • model: The Bedrock model ID (must support Tool Use)
  • aws_region: AWS region where Bedrock is available
  • response_format: Your Pydantic model or dict schema - this enables structured outputs

Note: When response_format is provided, AG2 automatically: 1. Converts your schema into a Bedrock tool definition 2. Forces the model to use this tool via toolChoice 3. Extracts and validates the structured response

Part 3: Create an Agent with Structured Outputs#

Create a ConversableAgent that will return structured responses:

# Create agent with structured output capability
math_agent = ConversableAgent(
    name="math_assistant",
    llm_config=llm_config,
    system_message="""You are a helpful math assistant that solves problems step by step.
    Always show your reasoning process clearly with explanations for each step.
    Return your response in the structured format requested.""",
    max_consecutive_auto_reply=1,
    human_input_mode="NEVER",
)

print(f"Agent '{math_agent.name}' created successfully!")

Part 4: Example 1 - Simple Equation with Structured Output#

Let’s solve a simple equation and see the structured response:

print("=== Example 1: Solve equation with structured output ===")

# Initiate chat with the agent
result1 = math_agent.run(
    message="Solve the equation: 2x + 5 = -25.",
    max_turns=5,
).process()

Now let’s parse and validate the structured response:

Part 5: Example 2 - Complex Math Problem#

Let’s try a more complex problem:

print("=== Example 2: Complex math problem ===")

result2 = math_agent.run(
    message="Designed to cause debate; some solve \\((2+2)\\) first (4), then \\(8\\div 2\\) (4), then \\(4\times 4\\) (16); others do \\(2\times 4\\) (8) first, then \\(8\\div 8\\) (1). use 10 steps to solve",
    max_turns=5,
).process()

Part 6: Using Dict Schema Instead of Pydantic Model#

You can also use a plain dictionary schema instead of a Pydantic model. This is useful when: - You don’t need Pydantic’s validation features - You want more flexibility in schema definition - You’re working with dynamic schemas

Let’s create a different schema for a different use case:

# Define schema as a dictionary (JSON Schema format)
dict_schema = {
    "type": "object",
    "properties": {
        "problem": {"type": "string", "description": "The math problem being solved"},
        "solution_steps": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {"step": {"type": "string"}, "result": {"type": "string"}},
                "required": ["step", "result"],
            },
        },
        "answer": {"type": "string"},
    },
    "required": ["problem", "solution_steps", "answer"],
}

print("Dict schema defined:")
print(json.dumps(dict_schema, indent=2))
# Create a new LLM config with dict schema
llm_config_dict = LLMConfig(
    config_list={
        "api_type": "bedrock",
        "model": "eu.anthropic.claude-3-7-sonnet-20250219-v1:0",
        "api_key": os.getenv("BEDROCK_API_KEY"),
        "aws_region": os.getenv("AWS_REGION", "us-east-1"),
        "aws_access_key": os.getenv("AWS_ACCESS_KEY_ID"),
        "aws_secret_key": os.getenv("AWS_SECRET_ACCESS_KEY"),
        "response_format": dict_schema,  # Using dict schema instead of Pydantic model
    },
)

# Create agent with dict schema
math_agent_dict = ConversableAgent(
    name="math_assistant_dict",
    llm_config=llm_config_dict,
    system_message="You are a helpful math assistant.",
    max_consecutive_auto_reply=1,
    human_input_mode="NEVER",
)

print("Agent created with dict schema!")
print("=== Example 3: Using dict schema ===")

result3 = math_agent_dict.run(
    message="Solve: x^2 - 5x + 6 = 0",
    max_turns=5,
).process()

Part 7: Understanding How It Works Under the Hood#

When you use response_format with Bedrock, AG2:

  1. Converts your schema to a tool: Your Pydantic model or dict schema becomes a Bedrock tool definition
  2. Forces tool usage: Sets toolChoice to force the model to call the structured output tool
  3. Extracts the data: Gets the structured data from the tool call’s input
  4. Validates: If using Pydantic, validates the data against your model
  5. Formats: Returns the JSON string (or formatted string if your model has a format() method)

Let’s inspect what the tool configuration looks like:

# Inspect the tool that gets created from your schema
from autogen.oai.bedrock import BedrockClient

# Create a temporary client to see the tool creation
temp_client = BedrockClient(
    aws_region=os.getenv("AWS_REGION", "us-east-1"),
    aws_access_key=os.getenv("AWS_ACCESS_KEY_ID"),
    aws_secret_key=os.getenv("AWS_SECRET_ACCESS_KEY"),
    response_format=MathReasoning,
)

# See how the schema is converted to a tool
tool = temp_client._create_structured_output_tool(MathReasoning)

print("Tool definition created from MathReasoning schema:")
print(json.dumps(tool, indent=2))

Notice that: - The tool name is "__structured_output" (a reserved name) - The inputSchema contains your JSON schema - The description explains it’s for structured output generation

Part 9: Best Practices#

1. Choose the Right Model#

  • Use Claude models (they have excellent tool use support)
  • Check model compatibility before using structured outputs

2. Schema Design#

  • Keep schemas simple and focused
  • Use descriptive field names and descriptions
  • Make required fields explicit

3. Error Handling#

  • Always wrap parsing in try/except blocks
  • Provide fallback behavior when structured output fails
  • Log errors for debugging

4. Pydantic vs Dict Schema#

  • Use Pydantic when you need:
    • Type validation
    • Automatic serialization/deserialization
    • IDE autocomplete
    • Custom formatting methods
  • Use Dict Schema when you need:
    • Dynamic schemas
    • Simpler setup
    • No external dependencies

5. Performance Considerations#

  • Structured outputs add a small overhead (tool call)
  • Consider caching for repeated queries
  • Use cache_seed for reproducible results during development

Part 10: Advanced Example - Custom Formatting#

You can add custom formatting methods to your Pydantic models for better display:

class DetailedMathReasoning(BaseModel):
    """Enhanced math reasoning with custom formatting."""

    problem: str
    steps: list[Step]
    final_answer: str
    verification: str | None = None

    def format(self) -> str:
        """Custom formatted output with problem statement."""
        output = f"Problem: {self.problem}\n\n"
        output += "Solution Steps:\n"
        for i, step in enumerate(self.steps, 1):
            output += f"  {i}. {step.explanation}\n"
            output += f"     β†’ {step.output}\n"
        output += f"\nFinal Answer: {self.final_answer}"
        if self.verification:
            output += f"\n\nVerification: {self.verification}"
        return output

    def to_markdown(self) -> str:
        """Export as Markdown format."""
        md = f"## Problem\n\n{self.problem}\n\n"
        md += "## Solution\n\n"
        for i, step in enumerate(self.steps, 1):
            md += f"### Step {i}\n\n"
            md += f"**Explanation**: {step.explanation}\n\n"
            md += f"**Result**: `{step.output}`\n\n"
        md += f"## Final Answer\n\n`{self.final_answer}`"
        return md

# Create agent with enhanced model
enhanced_llm_config = LLMConfig(
    config_list={
        "api_type": "bedrock",
        "model": "eu.anthropic.claude-3-7-sonnet-20250219-v1:0",
        "aws_region": os.getenv("AWS_REGION", "us-east-1"),
        "aws_access_key": os.getenv("AWS_ACCESS_KEY_ID"),
        "aws_secret_key": os.getenv("AWS_SECRET_ACCESS_KEY"),
        "response_format": DetailedMathReasoning,
    },
)

enhanced_agent = ConversableAgent(
    name="enhanced_math_assistant",
    llm_config=enhanced_llm_config,
    system_message="You are a detailed math assistant. Always verify your answers.",
    max_consecutive_auto_reply=1,
)

print("Enhanced agent created with custom formatting!")
# Test the enhanced agent
result = enhanced_agent.run(
    recipient=enhanced_agent,
    message="Solve: 4x - 8 = 12. Show your work and verify the answer.",
    max_turns=10,
).process()
from autogen.agentchat.group.patterns import AutoPattern

# Create a simple reviewer agent (without structured output) to work with the enhanced agent
reviewer_llm_config = LLMConfig(
    config_list={
        "api_type": "bedrock",
        "model": "eu.anthropic.claude-3-7-sonnet-20250219-v1:0",
        "aws_region": os.getenv("AWS_REGION", "us-east-1"),
        "aws_access_key": os.getenv("AWS_ACCESS_KEY_ID"),
        "aws_secret_key": os.getenv("AWS_SECRET_ACCESS_KEY"),
        # No response_format - this agent will use regular text responses
    },
)

reviewer_agent = ConversableAgent(
    name="math_reviewer",
    llm_config=reviewer_llm_config,
    system_message="You are a math reviewer. Review the solutions provided by the math assistant and provide feedback. Keep your reviews brief and constructive.",
    max_consecutive_auto_reply=1,
)

# Create AutoPattern for groupchat
pattern = AutoPattern(
    initial_agent=enhanced_agent,
    agents=[enhanced_agent, reviewer_agent],
    group_manager_args={
        "llm_config": enhanced_llm_config,  # Use same config for group manager
    },
)

print("AutoPattern created with structured output agent!")

Groupchat example#

import os

from dotenv import load_dotenv
from pydantic import BaseModel

from autogen import ConversableAgent, LLMConfig, UserProxyAgent
from autogen.agentchat import initiate_group_chat
from autogen.agentchat.group.patterns.auto import AutoPattern

load_dotenv()

# Define structured output models
class TaskDetails(BaseModel):
    """Details about the task being processed."""

    task_type: str
    description: str
    priority: str | None = None
    requirements: list[str] = []

class RoutingDecision(BaseModel):
    """Structured routing decision from the orchestrator."""

    request_analysis: str
    task_details: TaskDetails
    selected_agent: str
    routing_reason: str
    expected_outcome: str
    next_steps: list[str] = []

class WorkflowStatus(BaseModel):
    """Status of the current workflow execution."""

    current_stage: str
    completed_stages: list[str] = []
    pending_stages: list[str] = []
    issues: list[str] = []
    progress_percentage: int | None = None

class PipelineOrchestrationResponse(BaseModel):
    """Complete structured response from the pipeline orchestrator."""

    routing_decision: RoutingDecision
    workflow_status: WorkflowStatus | None = None

    def format(self) -> str:
        """Format the structured output for human-readable display."""
        output = "🎯 Pipeline Orchestration Decision\n"
        output += f"{'=' * 60}\n\n"
        output += f"Request Analysis:\n{self.routing_decision.request_analysis}\n\n"
        output += f"Task Type: {self.routing_decision.task_details.task_type}\n"
        output += f"Description: {self.routing_decision.task_details.description}\n\n"
        output += "Routing Decision:\n"
        output += f"  β†’ Selected Agent: {self.routing_decision.selected_agent}\n"
        output += f"  β†’ Reason: {self.routing_decision.routing_reason}\n"
        output += f"  β†’ Expected Outcome: {self.routing_decision.expected_outcome}\n"
        if self.routing_decision.next_steps:
            output += "\nNext Steps:\n"
            for i, step in enumerate(self.routing_decision.next_steps, 1):
                output += f"  {i}. {step}\n"
        if self.workflow_status:
            output += "\nWorkflow Status:\n"
            output += f"  Current Stage: {self.workflow_status.current_stage}\n"
            if self.workflow_status.completed_stages:
                output += f"  Completed: {', '.join(self.workflow_status.completed_stages)}\n"
            if self.workflow_status.pending_stages:
                output += f"  Pending: {', '.join(self.workflow_status.pending_stages)}\n"
        return output

# Regular LLM config for other agents
llm_config = LLMConfig(
    config_list={
        "api_type": "bedrock",
        "model": "eu.anthropic.claude-3-7-sonnet-20250219-v1:0",
        "api_key": os.getenv("BEDROCK_API_KEY"),
        "aws_region": os.getenv("AWS_REGION"),
        "aws_access_key": os.getenv("AWS_ACCESS_KEY"),
        "aws_secret_key": os.getenv("AWS_SECRET_ACCESS_KEY"),
        "aws_profile_name": os.getenv("AWS_PROFILE"),
    },
    cache_seed=42,
)

orchestrator_llm_config = LLMConfig(
    config_list={
        "api_type": "bedrock",
        "model": "eu.anthropic.claude-3-7-sonnet-20250219-v1:0",
        "api_key": os.getenv("BEDROCK_API_KEY"),
        "aws_region": os.getenv("AWS_REGION"),
        "aws_access_key": os.getenv("AWS_ACCESS_KEY"),
        "aws_secret_key": os.getenv("AWS_SECRET_ACCESS_KEY"),
        "aws_profile_name": os.getenv("AWS_PROFILE"),
        "response_format": PipelineOrchestrationResponse,
    },
    cache_seed=42,
)

print("Bedrock LLM configuration created with structured outputs!")

orchestrator = ConversableAgent(
    name="pipeline_orchestrator",
    system_message="""🎯 You are the Pipeline Orchestrator. Your role is to:
    β€’ Analyze user requests and determine the workflow path
    β€’ Route tasks to appropriate specialized agents
    β€’ Monitor pipeline progress and coordinate handoffs
    β€’ Report final results to the user

    You MUST provide structured routing decisions that include:
    - Analysis of the user's request
    - Task type and details
    - Selected agent and reasoning
    - Expected outcome and next steps

    Workflow Decision Logic:
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ 1. New project request? β†’ Route to project_creator      β”‚
    β”‚ 2. Code development? β†’ Route to code_developer          β”‚
    β”‚ 3. Code analysis needed? β†’ Route to code_quality_analyzerβ”‚
    β”‚ 4. Configuration updates? β†’ Route to config_manager     β”‚
    β”‚ 5. Build/deploy request? β†’ Route to build_deploy_agent  β”‚
    β”‚ 6. Validation needed? β†’ Route to deployment_validator   β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    Always provide clear routing decisions with reasoning.""",
    llm_config=orchestrator_llm_config,  # Use config with structured output
)

project_creator = ConversableAgent(
    name="project_creator",
    system_message="""πŸ—οΈ You are the Project Creator. Your role is to:
    Use the APPLY_PATCH tool to:
    βœ“ Create project structure and directories
    βœ“ Generate initial files (README.md, .gitignore, LICENSE)
    βœ“ Set up configuration files (package.json, requirements.txt, pom.xml)
    βœ“ Create initial source code templates
    βœ“ Set up test directory structure
    Project Creation Checklist:
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ β–‘ Project directory structure created?       β”‚
    β”‚ β–‘ Configuration files initialized?           β”‚
    β”‚ β–‘ Initial source code files created?         β”‚
    β”‚ β–‘ Test directory structure set up?           β”‚
    β”‚ β–‘ Documentation files added?                 β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    After project creation:
    β†’ Route to code_developer for implementation
    Create clean, well-structured project foundations.""",
    llm_config=llm_config,
)

code_developer = ConversableAgent(
    name="code_developer",
    system_message="""πŸ’» You are the Code Developer. Your role is to:
    Use the APPLY_PATCH tool to:
    βœ“ Write application code and implement features
    βœ“ Create modules, classes, and functions
    βœ“ Add business logic and algorithms
    βœ“ Implement API endpoints and routes
    βœ“ Write unit tests for new code
    Development Guidelines:
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ 1. Follow best practices and coding standardsβ”‚
    β”‚ 2. Write clean, maintainable code            β”‚
    β”‚ 3. Include proper error handling             β”‚
    β”‚ 4. Add comprehensive unit tests              β”‚
    β”‚ 5. Document complex logic                    β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

    After development:
    β†’ Route to code_quality_analyzer for testing and validation
    Write production-ready code with tests!""",
    llm_config=llm_config,
)

code_analyzer = ConversableAgent(
    name="code_quality_analyzer",
    system_message="""πŸ” You are the Code Quality Analyzer. Your role is to:
    Use the SHELL tool to:
    βœ“ Run test suites: pytest, unittest, npm test, mvn test
    βœ“ Run linters: pylint, flake8, black --check, eslint, prettier
    βœ“ Check git status: git status, git diff, git log
    βœ“ Analyze code coverage: coverage report, pytest --cov
    βœ“ Check build status: npm run build, mvn compile
    Analysis Checklist:
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ β–‘ All tests passing?                         β”‚
    β”‚ β–‘ No linting errors?                         β”‚
    β”‚ β–‘ Code coverage acceptable?                  β”‚
    β”‚ β–‘ No merge conflicts?                        β”‚
    β”‚ β–‘ Build successful?                          β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    Routing Rules:
    β€’ Issues found β†’ Route to code_fixer
    β€’ All checks pass β†’ Route to config_manager for CI/CD setup
    Provide detailed analysis reports with specific issues identified.""",
    llm_config=llm_config,
)

user = UserProxyAgent(
    name="user", human_input_mode="TERMINATE", code_execution_config={"work_dir": "coding", "use_docker": False}
)

pattern = AutoPattern(
    initial_agent=orchestrator,
    agents=[
        orchestrator,
        project_creator,
        code_developer,
        code_analyzer,
    ],
    user_agent=user,
    group_manager_args={"llm_config": orchestrator_llm_config},
)

result, context, last_agent = initiate_group_chat(
    pattern=pattern,
    messages="I want to create a new project. for a coffee machine business",
    max_rounds=5,
)
print(result)

Summary#

In this notebook, we’ve learned:

  1. βœ… How to define structured output schemas using Pydantic models
  2. βœ… How to configure Bedrock with response_format for structured outputs
  3. βœ… How to create agents that return structured, parseable responses
  4. βœ… How to parse and validate structured responses
  5. βœ… How to use dict schemas as an alternative to Pydantic
  6. βœ… How structured outputs work under the hood with Bedrock Tool Use
  7. βœ… Best practices for error handling and schema design
  8. βœ… Advanced techniques like custom formatting methods

Next Steps#

  • Try creating your own structured output schemas for different use cases
  • Experiment with different Bedrock models that support Tool Use
  • Combine structured outputs with other AG2 features like multi-agent conversations
  • Explore using structured outputs for data extraction and analysis tasks

References#