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
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
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!"
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'
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"})
}
}
}
}
`

Top comments (0)