DEV Community

Esteban
Esteban

Posted on

Building Agentic AI with Amazon Bedrock – Part 1: Your First AI Agent (Beginner Friendly)

Imagine an AI that doesn't just answer questions, but actually "does things" for you, for example, checking the weather, booking appointments, or analyzing data. These are called agentic AI systems, and they're transforming how we interact with artificial intelligence.

BTW, this is Part 1 of a four-part project series that I am building.

Overview

This series walks you step-by-step through building real agentic AI systems, from your first tool call to a full production-ready knowledge assistant.

  • Project 1 — Weather Agent (Beginner) -> Learn the core agent pattern: Define a tool → Call the model → Handle tool_use → Return results to Claude.

  • Project 2 — Research Assistant (Intermediate) -> Introduce multi-tool orchestration and build an agentic loop that handles multiple tool calls.

  • Project 3 — Task Planner (Advanced) -> Your agent plans its own execution, retries on failure, and reflects on improvements.

  • Project 4 — Knowledge Base Agent (Production) -> Use Bedrock’s managed agents + RAG to build enterprise-ready assistants.


My Approach: Applying Infrastructure as Code with Python

Why Python Scripts Instead of Manual Console Work?

Throughout this tutorial, you'll notice that I use Python scripts to create and configure AWS resources rather than clicking through the AWS Console. This is intentional and reflects real-world best practices. Here's why:

Repeatability

Why moving out from the GUI Approach:

• Make a mistake? Start over, clicking through 20+ screens
• Want to create in another region? Repeat all steps manually
• Team member needs to replicate? Send them a 50-step document
Enter fullscreen mode Exit fullscreen mode

Why is better with Python Scripting Approach:

• Make a mistake? Fix the script and re-run
• Different region? Change one variable
• Share with team? Send them the script
Enter fullscreen mode Exit fullscreen mode

Project 1: Build Your First AI Agent With Amazon Bedrock

In this project you'll build your first AI agent from scratch using Amazon Bedrock and AWS Lambda. By the end, you'll have a fully functional weather assistant that:

  • Understands natural language requests
  • Calls an external API to get real-time data
  • Synthesizes information into helpful responses

What you'll learn:

  • What Amazon Bedrock is and how it works
  • How to create AI agents that can use tools
  • How to build Lambda functions as agent tools
  • How to connect everything using Action Groups
  • How to test and deploy your agent

Prerequisites:

  • AWS account
  • Basic Python knowledge (or use AI to help you, as I did)
  • Terminal/command line familiarity
  • 20-40 minutes of your time

Key Concepts

Foundation Models: Pre-trained AI models you can use via API. Bedrock offers:

  • Claude (Anthropic) - Advanced reasoning and tool use
  • Llama (Meta)
  • Amazon Titan

Agents: AI systems that can:

  • Understand natural language
  • Decide when to use tools
  • Execute actions
  • Synthesize responses

Action Groups: Collections of tools (functions) your agent can use. Think of them as the agent's "superpowers."

Tools: Functions your agent can call, like:

  • Checking weather
  • Querying databases
  • Calling APIs
  • Processing data

How Bedrock Agents Work: The Big Picture

User asks: "What's the weather in NYC?"
          ↓
    [Bedrock Agent]
    - Powered by Claude
    - Has your instructions
    - Knows about action groups
          ↓
    Agent thinks: "I need weather data. 
                   I have a 'get_weather' action. 
                   I'll use that!"
          ↓
    [Action Group]
    - Knows this action calls a Lambda function
          ↓
    [Lambda Function]
    - Calls weather API
    - Gets real data
    - Returns it
          ↓
    [Back to Agent]
    - Receives weather data
    - Crafts natural response
          ↓
    User gets: "It's 52°F and rainy in New York City. 
                Bring an umbrella!"
Enter fullscreen mode Exit fullscreen mode

Enable Bedrock Model Access

Before we start coding, check if you have access to Claude (or your AI model of preference)

aws bedrock list-foundation-models \
--region us-east-1 \
--query 'modelSummaries[?contains(modelId,anthropic.claude`)].modelId'
Enter fullscreen mode Exit fullscreen mode

Note: If you see an error, go to AWS Console → Bedrock → Model Access → Request Access to Claude 3.5 Sonnet models


Building the Lambda Function

What is Lambda? AWS Lambda is a service that runs your code without you managing servers.

Perfect for our use case because:
- Agent calls Lambda only when user asks about weather
- Could be once an hour or 1000 times an hour - Lambda scales automatically
- You pay only for actual requests (free tier: 1M requests/month)

Create lambda_function.py:

`
import json
import os
import urllib.request
import urllib.parse
from typing import Tuple, Optional, Dict

USER_AGENT = "WeatherAssistant/1.0"

def http_get_json(url: str, headers: Optional[Dict] = None, timeout: int = 8) -> dict:
req = urllib.request.Request(url, headers=headers or {})
with urllib.request.urlopen(req, timeout=timeout) as resp:
return json.loads(resp.read().decode("utf-8"))

def geocode_city(city: str) -> Optional[Tuple[float, float]]:
if not city:
return None
params = urllib.parse.urlencode({"q": city, "format": "json", "limit": 1})
url = f"https://nominatim.openstreetmap.org/search?{params}"
headers = {"User-Agent": USER_AGENT}
try:
results = http_get_json(url, headers=headers)
if not results:
return None
return float(results[0]["lat"]), float(results[0]["lon"])
except Exception as e:
print(f"[ERROR] Geocoding failed: {e}")
return None

def get_grid_endpoints(lat: float, lon: float) -> Tuple[str, str]:
url = f"https://api.weather.gov/points/{lat:.4f},{lon:.4f}"
headers = {"User-Agent": USER_AGENT, "Accept": "application/geo+json"}
data = http_get_json(url, headers=headers)
props = data.get("properties", {})
return props["forecast"], props["forecastHourly"]

def get_forecast(url: str) -> dict:
headers = {"User-Agent": USER_AGENT, "Accept": "application/geo+json"}
return http_get_json(url, headers=headers)

def summarize_hourly_24h(hourly_json: dict) -> str:
periods = hourly_json.get("properties", {}).get("periods", [])
if not periods:
return "No hourly forecast available."
lines = []
for p in periods[:24]:
temp = p.get("temperature")
unit = p.get("temperatureUnit", "F")
short = p.get("shortForecast", "")
wind = f"{p.get('windSpeed', '')} {p.get('windDirection', '')}".strip()
lines.append(f"{temp}°{unit}, {short}, wind {wind}")
return "Next 24 hours: " + " | ".join(lines[:6])

def lambda_handler(event, context):
print(f"Received event: {json.dumps(event, default=str)}")

api_path = event.get('apiPath', '/weather') # Extract parameters from Bedrock agent format
http_method = event.get('httpMethod', 'GET')
parameters = event.get('parameters', [])

# Parse parameters
city = None
mode = "hourly"

for param in parameters:
    if param['name'] == 'city':
        city = param['value']
    elif param['name'] == 'mode':
        mode = param['value']

print(f"Parsed: city={city}, mode={mode}")

# Validate input
if not city:
    return {
        "messageVersion": "1.0",
        "response": {
            "actionGroup": event.get('actionGroup', 'weather-tools'),
            "apiPath": api_path,
            "httpMethod": http_method,
            "httpStatusCode": 400,
            "responseBody": {
                "application/json": {
                    "body": json.dumps({"error": "Missing required parameter: city"})
                }
            }
        }
    }

try:
    # Geocode city
    coords = geocode_city(city)
    if not coords:
        return {
            "messageVersion": "1.0",
            "response": {
                "actionGroup": event.get('actionGroup', 'weather-tools'),
                "apiPath": api_path,
                "httpMethod": http_method,
                "httpStatusCode": 404,
                "responseBody": {
                    "application/json": {
                        "body": json.dumps({"error": f"Could not find city: {city}"})
                    }
                }
            }
        }

    lat, lon = coords
    print(f"Geocoded '{city}' to lat={lat}, lon={lon}")

    # Get weather
    forecast_url, hourly_url = get_grid_endpoints(lat, lon)
    data = get_forecast(hourly_url if mode == "hourly" else forecast_url)
    weather_report = summarize_hourly_24h(data)

    # Return in correct Bedrock format
    return {
        "messageVersion": "1.0",
        "response": {
            "actionGroup": event.get('actionGroup', 'weather-tools'),
            "apiPath": api_path,  # CRITICAL: Must match input
            "httpMethod": http_method,
            "httpStatusCode": 200,
            "responseBody": {
                "application/json": {
                    "body": json.dumps({"weather_report": weather_report})
                }
            }
        }
    }

except Exception as e:
    print(f"[ERROR] {e}")
    return {
        "messageVersion": "1.0",
        "response": {
            "actionGroup": event.get('actionGroup', 'weather-tools'),
            "apiPath": api_path,
            "httpMethod": http_method,
            "httpStatusCode": 500,
            "responseBody": {
                "application/json": {
                    "body": json.dumps({"error": "Weather service error"})
                }
            }
        }
    }
Enter fullscreen mode Exit fullscreen mode

`

Top comments (0)