Author: broadoakdata

  • Building Enterprise Multi-Agent Systems with AWS Strands SDK

    The landscape of Generative AI is shifting. We are moving away from simple “Chat with PDF” bots (RAG) toward Agentic Workflows—systems where AI doesn’t just retrieve information but actively plans, reasons, and executes tasks.

    However, building these systems on the cloud involves choosing between two distinct architectures: Client-Side (Code-First) and Server-Side (Infrastructure-First).

    In this guide, we will explore both approaches to building an Energy Management System where a Supervisor Agent orchestrates a team of specialized workers.

    The Architecture: Supervisor & Workers

    Instead of asking one giant LLM to know everything, we split the responsibility:

    1. The Forecasting Agent: A specialist in data. It has tools to retrieve historical energy usage and perform mental math to predict future trends.
    2. The Solar Support Agent: A specialist in hardware. It has access to technical manuals and troubleshooting guides.
    3. The Supervisor: The orchestrator. It analyzes the user’s intent and delegates tasks to the correct specialist.

    Approach 1: The Client-Side (AWS Strands SDK)

    The AWS Strands SDK is an open-source, code-first framework. In this model, the agent logic lives in your Python application, not on AWS servers.

    1. Setup & Configuration

    First, we initialize the “Brain” using Amazon Bedrock. We use Claude 3 Sonnet for its balance of reasoning capability and speed.

    import json
    from strands import Agent, tool
    from strands.models import BedrockModel
    
    # Configure the Bedrock Model
    llm = BedrockModel(
        model_id="anthropic.claude-3-sonnet-20240229-v1:0",
        region_name="us-east-1",
        temperature=0.0 # Keep it deterministic for data tasks
    )
    

    2. Defining Tools (The “Hands”)

    In Strands, tools are just Python functions decorated with @tool.

    @tool
    def get_energy_history(customer_id: str) -> str:
        """
        Retrieves the last 12 months of energy usage for a customer.
        Returns a JSON string of monthly kWh values.
        """
        # Mock Data for demonstration
        if customer_id != "1":
            return "Error: Customer not found."
        
        data = [
            {"month": "Jan", "kwh": 158}, 
            {"month": "Feb", "kwh": 144},
            # ... (truncated for brevity)
            {"month": "Dec", "kwh": 175}
        ]
        return json.dumps(data)
    
    @tool
    def search_technical_manual(query: str) -> str:
        """
        Searches the Solar Panel Technical Manual for maintenance procedures.
        """
        q = query.lower()
        if "clean" in q:
            return "MAINTENANCE PROCEDURE: Clean panels twice a year using a soft cloth."
        elif "error" in q:
            return "TROUBLESHOOTING: Error 001 = Grid Disconnect."
        return "No info found."
    

    3. Creating the Agents

    We wrap our sub-agents in functions so the Supervisor can call them, utilizing the Agent-as-a-Tool pattern.

    # The Sub-Agents
    forecaster = Agent(
        name="ForecastingAgent",
        model=llm,
        tools=[get_energy_history],
        system_prompt="You are an expert Energy Forecaster."
    )
    
    solar_expert = Agent(
        name="SolarAgent",
        model=llm,
        tools=[search_technical_manual],
        system_prompt="You are a Solar Panel Technician."
    )
    
    # The Supervisor Wrapper
    @tool
    def ask_forecaster(question: str) -> str:
        """Delegates questions about energy data/bills to the Forecaster."""
        return str(forecaster(question))
    
    @tool
    def ask_solar_expert(question: str) -> str:
        """Delegates questions about solar panels to the Solar Expert."""
        return str(solar_expert(question))
    
    # The Boss
    supervisor = Agent(
        name="Supervisor",
        model=llm,
        tools=[ask_forecaster, ask_solar_expert],
        system_prompt="You are the Energy Team Supervisor. Delegate to your team."
    )
    

    Approach 2: The Server-Side (AWS Bedrock Agents)

    Alternatively, you can build this as a Managed Service. In this model, the agents exist as cloud resources inside AWS. The tools are AWS Lambda functions, and the orchestration is handled entirely by Amazon Bedrock.

    The Backend Code (Python/Boto3)

    Instead of defining agents as objects, we define them via API calls.

    import boto3
    
    bedrock_agent = boto3.client('bedrock-agent')
    
    def create_server_side_supervisor(forecast_alias_arn, solar_alias_arn):
        # 1. Create the Supervisor Resource
        resp = bedrock_agent.create_agent(
            agentName="Energy-Supervisor",
            foundationModel="anthropic.claude-3-5-sonnet-20240620-v1:0", 
            instruction="Route data questions to ForecastingAgent and manual questions to SolarPanelAgent.",
            agentCollaboration='SUPERVISOR' # <--- Enables Server-Side Orchestration
        )
        supervisor_id = resp['agent']['agentId']
        
        # 2. Link Sub-Agents (Must be deployed Aliases)
        bedrock_agent.associate_agent_collaborator(
            agentId=supervisor_id,
            agentVersion='DRAFT',
            agentDescriptor={'aliasArn': forecast_alias_arn},
            collaboratorName='ForecastingAgent',
            collaborationInstruction='Use for historical data.',
            relayConversationHistory='TO_COLLABORATOR'
        )
        
        return supervisor_id
    

    Production Deployment: Where does the code live?

    You might be asking: If I use the Strands SDK (Approach 1), where do I actually run this Python code in production?

    This is where the ecosystem is evolving rapidly. You have two main paths:

    1. The Future: AWS Agentic Core (Managed Runtime)

    AWS is introducing Agentic Core, a specialized managed runtime designed specifically for hosting agents built with frameworks like Strands.

    • The Fit: Strands provides the logic (the code), and Agentic Core provides the infrastructure (the hosting, memory, and identity management).
    • The Reality: As of now, Agentic Core is a newer service. While promising for handling persistent sessions and history automatically, many engineering teams prefer standard compute options for stability until the service matures to full production grade.

    2. The “Right Now” Solution: Docker & AWS App Runner

    Since your Strands agent is simply a Python application wrapped in an API (like FastAPI), you can deploy it today using standard container tools. This gives you full control and production-grade reliability immediately.

    Step 1: Containerize Create a Dockerfile for your Strands API:

    FROM python:3.11-slim
    WORKDIR /app
    COPY . .
    RUN pip install strands-agents strands-agents-tools boto3 fastapi uvicorn
    CMD ["uvicorn", "strands_api:app", "--host", "0.0.0.0", "--port", "8080"]
    

    Step 2: Deploy to AWS App Runner Instead of managing servers, point AWS App Runner to your container image (stored in Amazon ECR).

    • Why? It automatically scales your API based on traffic, handles HTTPS/TLS for you, and requires zero infrastructure management.
    • Result: You get a secure endpoint (e.g., https://my-agents.awsapprunner.com/chat) that your Streamlit frontend can call securely.

    Which Approach Should You Choose?

    Both architectures solve the same problem, but they serve different stages of the development lifecycle.

    FeatureClient-Side (Strands SDK)Server-Side (Bedrock Agents)
    Development SpeedFast. Changes are instant. Debugging uses standard Python tools.Slower. Requires deploying Lambdas, IAM roles, and waiting for Agent compilation.
    InfrastructureZero. Runs in your app container/server.Heavy. Requires Lambda, S3, and DynamoDB setup.
    State ManagementManual. Memory is lost when the script stops unless you save it.Managed. AWS automatically stores conversation history for 30 days.
    ScalabilityYour Responsibility. You must scale the server running the python code.Serverless. AWS scales automatically to thousands of concurrent users.
    Best For…Prototyping, complex logic, and local tools.Enterprise production, auditability, and massive scale.

    The Frontend: Streamlit

    Regardless of which backend you choose, you need a UI to visualize the multi-agent reasoning.

    import streamlit as st
    import requests
    
    # ... (Streamlit setup)
    
    if prompt := st.chat_input("Ask about energy usage..."):
        with st.chat_message("assistant"):
            with st.status("🤖 Supervisor is thinking...", expanded=True) as status:
                try:
                    # Call your API (Strands or Bedrock Wrapper)
                    response = requests.post(API_URL, json={"query": prompt})
                    status.update(label="Complete", state="complete", expanded=False)
                    st.markdown(response.json()['answer'])
                except Exception as e:
                    status.update(label="Error", state="error")
    

    Conclusion

    Multi-Agent systems represent the next evolution of Generative AI.

    If you are just starting or building internal tools, AWS Strands offers an incredible “Code-First” experience that lets you iterate in seconds. If you are deploying a mission-critical application for millions of users, AWS Bedrock Managed Agents provides the robust, serverless infrastructure you need.