AWS setup from zero
Everything in both modules runs on your own laptop and calls AI models hosted on AWS. So there are exactly two things to arrange: credentials (so your laptop can talk to AWS) and model access (so AWS lets you use the models). Nothing else.
Running code locally is free. You only pay per model call, priced per token — the examples in this entire masterclass cost a fraction of a cent in total. Nothing keeps running in the background, so there is nothing to switch off afterwards.
0.1 · Create an access key (browser, once)
Signed in to the AWS console as an admin:
- Top search bar → type IAM → open it.
- Left menu → Users → click your username.
- Open the Security credentials tab.
- Scroll to Access keys → Create access key.
- Use case → Command Line Interface (CLI) → tick the box → Next → Create access key.
- Copy the Access key ID and Secret access key now — the secret is shown only once.
Never paste it into chat, screenshots, or a Git repo. aws configure stores it
locally in ~/.aws/credentials, which is where it belongs.
0.2 · Connect your laptop
aws configure # AWS Access Key ID → paste the key id # AWS Secret Access Key → paste the secret # Default region name → us-east-1 # Default output format → (press Enter, leave blank)
Check it worked:
aws sts get-caller-identity
Success prints your Account, UserId, and Arn. If you get InvalidClientTokenId, the key is wrong or
deactivated — create a fresh one and run aws configure again.
0.3 · Enable the models in Bedrock
Models are switched off by default. In the console:
- Top-right region selector → US East (N. Virginia) · us-east-1.
- Search Bedrock → open it → left menu → Model access.
- Modify model access → tick the models below → submit.
| Model | Used for |
|---|---|
| Amazon Nova Lite | Module 1 Hello World (cheap, fast) |
| Claude Haiku 4.5 | Module 1 LangGraph example, Module 2 tools |
| Claude Sonnet 4.5 | Heavier reasoning; useful later |
Verify from the terminal which Anthropic models are live in your account:
aws bedrock list-foundation-models --region us-east-1 \
--query "modelSummaries[?contains(modelId,'anthropic.claude')].modelId" \
--output table
The repo pins older model IDs that AWS has since retired. You will see ResourceNotFoundException … model version has reached the end of its life or … marked by provider as Legacy. The fix is always the same: list the
live models with the command above, pick one, prefix it with us., and swap it into
the code. Treat this as a skill worth learning, not as a bug.
0.4 · The project folder
All the code for both modules ships with this guide, already fixed and ready to run. Unzip it anywhere and open the folder in VS Code — you can rename the folder to whatever you like, nothing depends on its name.
. ├── GUIDE.html # this file ├── README.md # how to run everything ├── config.py # model IDs — change once, applies everywhere ├── requirements.txt ├── setup.sh / setup.bat ├── 00_check_setup.py # run this FIRST ├── 01_list_models.py # use when a model is retired ├── shivank1/ # 2 examples ├── shivank2/ # 9 examples └── shivank3/ # capstone project
0.5 · Install the packages
# macOS / Linux ./setup.sh # Windows setup.bat
Or do it manually if you prefer:
python3 --version # must be 3.10 or higher python3 -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate pip install --upgrade pip pip install -r requirements.txt
(base))The venv keeps this project isolated from your conda environment. After activating you should see (.venv) at the front of your prompt. In VS Code, also pick the interpreter: ⌘⇧P → Python: Select
Interpreter → choose the .venv one.
0.6 · Run the readiness check
This is the single most useful command in the whole project. It verifies Python, packages, credentials, region, Bedrock access, and makes a real model call — so if it passes, every example will run.
source .venv/bin/activate export AWS_DEFAULT_REGION=us-east-1 python 00_check_setup.py
Six [ OK ] lines ending with "All checks passed." If anything fails it
prints the exact fix — enable model access, paste fresh credentials, or swap a retired model. Run this
before you touch anything else. If it passes, every example in this guide will work.
Give Section 0 a proper 15 minutes before you write any agent code. Setup failures are the number-one
reason people abandon hands-on AI work. Do not move past this section until get-caller-identity returns your account details.
Why agents exist at all
Start here, because it frames everything else. A standard LLM has two hard limits:
- Knowledge cutoff — it only knows what it was trained on. Ask about yesterday's news and it cannot help.
- No access to your world — it cannot read your company database, check today's weather, or send an email.
There are two ways to fix this without retraining the model:
| Approach | What it adds | The model becomes… |
|---|---|---|
| RAG | Relevant context fetched from your data | A better-informed knowledge retriever |
| Agents | The ability to reason and use tools | An actor that gets things done |
Here is the framing that matters: many teams build a RAG system, then find users want the AI to actually do the work — book the meeting, update the record, run the workflow. Moving from passive retriever to active agent is exactly where people get stuck. That is what these two modules fix.
RAG and how it works
RAG combines an LLM with a search system in three steps:
- Retrieve — search an external knowledge base for information relevant to the question.
- Augment — glue that retrieved context onto the original question to form a richer prompt.
- Generate — the LLM answers using the supplied context.
For example: "Who won the 2024 Nobel Prize in Physics?" The system queries a real-time news database, finds the fact, adds it to the prompt, and the LLM produces an accurate answer — despite that fact being past its training cutoff.
The full RAG pipeline
The next diagram expands this into the seven stages of building a real RAG system.
The stages, in plain language
- Load — bring in the documents you want to query.
- Chunk — split them into small pieces. Two reasons: models have a limited context window, and it avoids the lost-in-the-middle effect where details buried in a long passage get overlooked.
- Embed — convert each chunk into a vector, a numerical representation of its meaning (using an embedding model such as Amazon Titan).
- Store — keep chunks plus vectors in a vector database that supports similarity search. Common examples: Amazon OpenSearch Service, Pinecone, FAISS, Chroma, PostgreSQL with pgvector.
- Retrieve — turn the user's question into a vector too, and pull the top-k most similar chunks (k = 3, 5, 10…).
- Filter & rerank — optionally re-sort for quality. Improves results but adds cost and complexity.
- Generate — hand the retrieved chunks plus the original question to the LLM, which writes the final answer.
RAG is an open-book exam. The model hasn't memorised the textbook, but you let it flip to the three most relevant pages before answering. Chunking is deciding how big each page is; embedding is the index at the back that tells you which pages to flip to.
What is an agent?
The word agent just means something that performs a task on your behalf. A working definition: AI agents are autonomous software systems that use AI to reason, plan, and carry out tasks for humans or other systems. They make decisions, adapt to new information, and act without needing explicit instructions for every step.
What makes them powerful is iterative thinking — they evaluate results, adjust, and keep working toward a goal. And they often use RAG as one part of that workflow.
Follow this example closely. The agent takes "What should I pack for New York summer?" and it does not just look one thing up. It:
- Uses RAG to retrieve historical weather data
- Checks a real-time forecast via an API
- Analyses user preferences (pack light, but avoid being cold)
- Synthesises all of it into a personalised recommendation
That final synthesis — combining three different sources into a judgement — is the decision-making that separates an agent from simple retrieval.
RAG answers questions. Agents accomplish goals. RAG gives the model better information; an agent gives it the ability to act, check the result, and try again.
From LLMs to multi-agent systems
Think of this as a progression. Each step adds one capability. This is the fastest way to orient yourself if you "sort of know" agents already.
The one line to hold on to: each level adds exactly one thing. Memory, then grounding, then action, then teamwork. If you can name what each level adds, you understand the shape of the entire field.
Agent protocols — MCP and A2A
Once agents need to reach the outside world and each other, you need standard ways to connect. Two protocols matter, and they are constantly confused with each other, so compare them side by side.
MCP — Model Context Protocol
MCP connects an agent to tools and data. A server exposes capabilities; a client (your agent) discovers and calls them. Think of it as USB for AI tools — plug in a new server, gain new abilities, without changing your agent's code.
A2A — Agent to Agent
A2A connects an agent to other agents. Each agent publishes an agent card (a small profile: name, skills, how to talk to it), and other agents read that card and send it messages.
| MCP | A2A | |
|---|---|---|
| Connects an agent to… | Tools, data, prompts | Other agents |
| The other side is… | A server exposing capabilities | A peer agent with its own brain |
| Discovery via | Listing tools/resources/prompts | The agent card |
| Analogy | USB port for abilities | Colleagues phoning each other |
Module 1 only introduces these protocols — building MCP servers and A2A agents is a much larger topic on its own. Learn the vocabulary here so it is familiar later, then move on. Resist the urge to go and build an MCP server today; finish the agent fundamentals first.
The AWS agentic stack
AWS offers three layers for building agents. Always know which layer you are standing on.
Amazon Bedrock is the key one for today: it is a managed service that hosts foundation models (Claude, Nova, and more) behind a single API. You enabled model access in Section 0 — that is what makes these models callable from your code.
The Strands SDK and the agentic loop
Put simply: Strands has three core components — model, tools, and prompt — plus an agentic feedback loop.
The loop in plain terms: the agent asks the model; the model reasons, responds, and selects tools; the agent executes those tools; results feed back into the agent, which may re-invoke the model. This continues until the agent determines the prompt has been fully addressed — then it compiles everything and returns the result.
"A chatbot answers once. An agent keeps going until the job is done." The loop is the difference. Everything else is detail.
Building a "Hello World" agent
Run everything from the project root (the folder containing config.py) — not from inside shivank1/. The scripts import
shared settings from config.py, so running from a subfolder gives ModuleNotFoundError: No module named 'config'.
8.1 · The simplest possible agent — Strands + Nova Lite
from strands import Agent from strands.models.bedrock import BedrockModel from config import NOVA_LITE # Nova Lite: Amazon's cheapest model — ideal for a first run. model = BedrockModel(model_id=NOVA_LITE) agent = Agent(model=model) response = agent("Hello! Tell me a fun fact about AI agents.") print(response)
python shivank1/01_hello_world_agent.py
Four lines is a whole agent. Read each one: choose a model, wrap it in an Agent, call the agent like a function, print the answer. There are no tools here yet, so
the loop runs exactly once — this is the "before" picture for Module 2.
8.2 · Same idea in LangGraph, with a tool
Strands is not the only framework. This version uses LangGraph and adds a small tool, so you can watch the loop actually loop.
from langchain.chat_models import init_chat_model from langchain.tools import tool from langgraph.prebuilt import create_react_agent from config import MODEL_ID # Define a simple tool @tool def greet(name: str) -> str: """Greet someone by name.""" return f"Hello, {name}! Welcome to the world of AI agents." # Initialize the LLM via Bedrock llm = init_chat_model( MODEL_ID, model_provider="bedrock_converse", ) # Create a ReAct agent with the tool agent = create_react_agent(model=llm, tools=[greet]) # Run the agent response = agent.invoke( {"messages": [{"role": "user", "content": "Please greet Alice and Bob."}]} ) # Print every step so you can see the loop for message in response["messages"]: print(f"{message.type}: {message.text}")
python shivank1/02_hello_world_langgraph.py
You will see five lines: human: the request · ai: (empty)
· tool: Hello, Alice! · tool: Hello, Bob! · ai: a summary. Stop and unpack this. The empty ai: line is the model choosing to use a tool instead of answering. That is the
agentic loop from Diagram 9, visible in your terminal.
Older tutorials pin anthropic.claude-3-5-haiku-20241022-v1:0, which AWS has since
retired. These files already use the current model via config.py,
so there is nothing to patch. If a model is retired in future, run python 01_list_models.py, pick a live one, and change the single line in config.py — every example picks it up.
LLM, agent, and tools — who does what
The division of labour — the cleanest mental model here:
- The LLM is the brain — it understands requests, reasons about what needs doing, and decides which tools to use.
- Tools are the hands and senses — they perform actions and gather information from the outside world.
- The agent is the coordinator — it manages the conversation between the LLM and the tools.
Types of tools
From function to tool
This is the most important idea in this module. Start with an ordinary Python function that checks whether a server is up:
import requests def check_server_status(server_url): """Check if a server is responding.""" try: response = requests.get(server_url, timeout=5) return f"Server is up. Status code: {response.status_code}" except requests.exceptions.RequestException: return "Server is down or unreachable" # You use it like this: status = check_server_status("https://staging.myapp.com") print(status) # "Server is up. Status code: 200"
Ask the agent "Is the staging server running?" and it replies that it has no ability to check server status. The agent cannot magically discover your function. That is the gap.
The bridge is function calling (also called tool use). Add three things:
| Add this | Why |
|---|---|
@tool decorator |
Tells Strands to make this function available to agents |
| Type hints | Tell the agent what data types to expect |
| A proper docstring | Describes what it does, so the agent knows when to use it |
from strands import Agent, tool import requests @tool # 1. decorator def check_server_status(server_url: str) -> str: # 2. type hints """Check if a server is responding by making an HTTP request. Args: server_url: The URL of the server to check Returns: A message indicating whether the server is up or down """ # 3. docstring try: response = requests.get(server_url, timeout=5) return f"Server is up. Status code: {response.status_code}" except requests.exceptions.RequestException: return "Server is down or unreachable"
agent = Agent(tools=[check_server_status])
response = agent("Is the staging server running? Check https://httpbin.org/get")
python shivank2/01_function_to_tool.py
The docstring is not a comment — it is the user manual the model reads. The model decides
whether to call your tool based on that description alone. Vague docstring, unreliable agent. Deliberately
break one: change the docstring to "Does a thing." and watch the agent stop calling
it. That single experiment will teach you more than any amount of reading.
In short: this pattern works for any function. Add @tool, give it to your Agent,
and the agent can now execute it.
Your first tool-enabled agent — the tip calculator
A practical, self-contained example from Module 2. Nothing external to configure.
from strands import Agent, tool @tool def calculate_tip(bill_amount: float, tip_percentage: float, num_people: int = 1) -> dict: """Calculate tip and split the bill among people. Args: bill_amount: Total bill amount in dollars tip_percentage: Tip percentage (e.g., 15, 18, 20) num_people: Number of people splitting the bill (default: 1) """ tip = bill_amount * (tip_percentage / 100) total = bill_amount + tip per_person = total / num_people return { "bill": bill_amount, "tip": round(tip, 2), "total": round(total, 2), "per_person": round(per_person, 2) } agent = Agent(tools=[calculate_tip]) response = agent("The bill is $85. What's a 20% tip, and how much does each person pay if we're splitting it 4 ways?") print(response.message['content'][0]['text'])
Then show that the agent understands intent, not keywords — all three of these work without any extra code:
agent("What's a 15% tip on $42?") agent("Bill is $120, we want to tip 18%, split between 3 people") agent("Calculate tip for $67.50 at 20%")
python shivank2/02_tip_calculator.py
In traditional chatbot development you'd write regex patterns and intent classifiers to handle those three phrasings, and separately extract the numbers. Here you wrote one function with a clear description and the model did the intent recognition and the parameter extraction. That is the leap.
When one tool isn't enough
The scenario: you ask a sales assistant to "pull last quarter's sales data and email a summary to the team." That is not one task — it is three: query the database, analyse the numbers, send an email.
from strands import Agent, tool @tool def get_sales_data(quarter: str) -> dict: """Retrieve sales data for a specific quarter.""" return {"revenue": 1250000, "deals": 47, "quarter": quarter} @tool def analyze_sales(revenue: int, deals: int, quarter: str) -> str: """Calculate key metrics from sales data.""" avg_deal = revenue / deals return f"Q{quarter}: ${revenue:,} revenue, {deals} deals, ${avg_deal:,.0f} avg deal size" @tool def send_email(to: str, subject: str, body: str) -> str: """Send an email message.""" return f"Email sent to {to}" agent = Agent(tools=[get_sales_data, analyze_sales, send_email]) response = agent("Pull last quarter's sales data and email a summary to the team")
You never told the agent the sequence. It recognised it needed data first, then analysis, then delivery — and chained them. That is planning.
python shivank2/03_multi_tool_sales.py
Build focused, single-purpose tools. Avoid the temptation to create one mega-tool that does everything: it
becomes harder to maintain, harder for the agent to reason about, and impossible to reuse elsewhere. Small
tools recombine — the same analyze_sales works with data from any source.
Pre-built community tools
You don't have to write everything. strands_tools ships ready-made tools — import
and go.
13.1 · Calculator
from strands import Agent from strands_tools import calculator agent = Agent( tools=[calculator], system_prompt="You are a helpful math assistant." ) agent("What's 42 raised to the power of 9?") agent("Solve the equation x^2 + 5x + 6 = 0") agent("What's the derivative of sin(x) * cos(x)?")
Note this also introduces the system prompt — standing instructions that shape the agent's behaviour across every request.
python shivank2/05_prebuilt_tools.py
13.2 · Combining several pre-built tools
Fetch data from the web, do maths on it, and write a file — one request, three tools.
import os from strands import Agent from strands_tools import http_request, calculator, file_write agent = Agent( tools=[http_request, calculator, file_write], system_prompt="You help with data analysis tasks." ) os.environ["BYPASS_TOOL_CONSENT"] = "true" agent(""" Fetch stock data from https://query1.finance.yahoo.com/v8/finance/chart/AAPL?interval=1d&range=5d, extract the latest closing prices, calculate the average price over the period, and save the results to stock_summary.txt """)
python shivank2/06_multi_prebuilt_tools.py
Some tools are sensitive — they write files or touch cloud resources — so Strands pauses and asks
permission before running them. Setting BYPASS_TOOL_CONSENT="true" turns
that prompt off so the cell runs unattended. Teach it as a real safety feature: in production you often
want a human approving actions. If a cell seems to hang with [*], it is
waiting for your y at a hidden prompt.
AWS integration with use_aws
One tool, many services. use_aws translates plain English into AWS API calls.
from strands import Agent from strands_tools import use_aws agent = Agent( tools=[use_aws], system_prompt="You are an AWS assistant that helps manage cloud resources." ) agent("List all S3 buckets in my account")
The same single tool handles completely different services — Other examples:
# DynamoDB agent("Look up customer ID 12345 in the DynamoDB customers table and update their email to newemail@example.com") # Lambda agent("Invoke the Lambda function 'order-processor' with order ID 67890")
These examples assume the AWS resources already exist in your account. The S3 bucket, DynamoDB table, and Lambda function referenced must exist for the snippets to work. While you are learning, stick to the S3 listing example — it works on any account, even an empty one (an empty list is a valid result, not an error).
Notice what just happened: you described what you wanted in plain English, and the agent figured out the AWS API calls. You didn't write boto3 code, didn't handle AWS responses, and didn't even specify which operation to use.
python shivank2/07_use_aws.py
use_aws can modify real resources. Stick to read-only requests ("list",
"describe") while you learn, and always work in a sandbox account, never production. This is exactly why the
consent prompt exists — read it before you type y.
Building custom tools
When community tools don't fit — an internal API, a proprietary database, something new — you write your own. For example: an online store checking inventory.
from strands import Agent, tool @tool def check_inventory(product_id: str) -> str: """Check if a product is in stock. Args: product_id: The product ID to check (e.g., "PROD-123") """ # This is where you'd query your actual database inventory = { "PROD-123": 15, "PROD-456": 0, "PROD-789": 8 } quantity = inventory.get(product_id, 0) if quantity > 0: return f"Product {product_id} is in stock. We have {quantity} units available." else: return f"Product {product_id} is currently out of stock." agent = Agent(tools=[check_inventory]) # All of these work — the agent understands intent, not just keywords agent("Is PROD-123 in stock?") agent("Do we have PROD-456 available?") agent("Check inventory for PROD-789") agent("Can I order PROD-123 right now?")
Note the mock dictionary: in production you'd replace it with real database queries or an API call. The agent-facing part — decorator, type hints, docstring — stays identical either way.
python shivank2/04_custom_tool_inventory.py
Build a tool for a problem you actually care about. Ideas: check the weather in your city (call a weather API) · validate email format · calculate age from birthdate · calculate BMI · calculate shipping costs from weight and distance. Don't worry if the logic is simple — what matters is seeing how a custom tool fits into the agent workflow.
When simple tools aren't enough
This module closes with three scenarios where the plain @tool approach strains.
Learn these as "recognise it when it happens", not as something to memorise.
16.1 · The database connection problem → class-based tools
The story: your agent has five tools, each opening its own database connection. Your DBA messages: "Why is your agent opening 50 database connections per minute?" Each tool call opens and closes a connection; multiply by concurrent users and the database drowns.
The fix: group related tools in a class so they share one connection.
from strands import Agent, tool class InventoryTools: def __init__(self): # Shared resource: all tools access the same data store. # In production: self.db = connect_to_database() self.products = { "PROD-123": {"name": "Wireless Mouse", "quantity": 15, "price": 29.99}, "PROD-456": {"name": "USB-C Hub", "quantity": 0, "price": 49.99}, "PROD-789": {"name": "Mechanical Keyboard", "quantity": 8, "price": 89.99}, } @tool def check_stock(self, product_id: str) -> str: """Check product stock level. Args: product_id: The product ID to check """ product = self.products.get(product_id) if not product: return f"Product {product_id} not found" return f"{product['name']}: {product['quantity']} units at ${product['price']}" @tool def update_stock(self, product_id: str, quantity: int) -> str: """Update product stock quantity. Args: product_id: The product ID to update quantity: New quantity to set """ if product_id in self.products: self.products[product_id]["quantity"] = quantity return f"Updated {product_id} to {quantity} units" return f"Product {product_id} not found" # One instance, shared state, multiple tools inventory = InventoryTools() agent = Agent(tools=[inventory.check_stock, inventory.update_stock]) agent("Check stock for PROD-123") agent("Update PROD-456 stock to 25 units, then confirm the new level")
python shivank2/08_class_based_tools.py
16.2 · Slow sequential calls → async tools
If three warehouse lookups take 2 seconds each, doing them one after another costs 6 seconds. Make the tool
async and they run in parallel.
import asyncio import time from strands import Agent, tool @tool async def check_warehouse_inventory(product_id: str, warehouse: str) -> dict: """Check inventory at a specific warehouse. Args: product_id: Product ID to check warehouse: Warehouse identifier (e.g., "east", "west", "central") """ # Simulate API call delay await asyncio.sleep(2) data = { "east": {"PROD-123": 45, "PROD-456": 12}, "west": {"PROD-123": 30, "PROD-456": 0}, "central": {"PROD-123": 60, "PROD-456": 25}, } quantity = data.get(warehouse, {}).get(product_id, 0) return { "warehouse": warehouse, "product_id": product_id, "quantity": quantity } async def main(): agent = Agent(tools=[check_warehouse_inventory]) start = time.time() response = await agent.invoke_async( "Can we ship 100 units of PROD-123? Check all warehouses: east, west, and central." ) elapsed = time.time() - start print(response.message['content'][0]['text']) print(f"\nTotal time: {elapsed:.1f}s (sequential would be ~6s)") await main()
python shivank2/09_async_tools.py
The printed timing is the lesson: roughly 2 seconds instead of 6. Run it yourself — you see
concurrency instead of just reading about it. (The bare await main() works in a
Jupyter cell; in a plain script use asyncio.run(main()).)
Project — build a Travel Assistant Agent
This project deliberately mirrors Diagram 3 (the travel planner), so you finish where Module 1 began — except this time you build it yourself. It exercises every skill from both modules: custom tools, multiple tools, a pre-built tool, a system prompt, and the agentic loop.
The brief
Build an agent that answers: "I'm going to Goa for 3 days next week with a budget of ₹20,000. What should I pack and what will it cost?" — and actually reasons across weather, packing, and budget to answer it.
| Tool | Job | Skill practised |
|---|---|---|
get_weather_forecast |
Return conditions for a city and date range | Custom tool, mock data |
suggest_packing_list |
Turn weather + trip length into a packing list | Tool that consumes another tool's output |
estimate_trip_cost |
Rough cost from city, days, travellers | Numeric logic + dict return |
calculator |
Budget maths | Pre-built community tool |
Starter code
"""Capstone: Travel Assistant Agent (Modules 1 & 2).""" from strands import Agent, tool from strands.models.bedrock import BedrockModel from strands_tools import calculator @tool def get_weather_forecast(city: str, days: int) -> dict: """Get the weather forecast for a city over a number of days. Args: city: Destination city name (e.g., "Goa", "Bangalore") days: Number of days in the trip """ # Mock data — replace with a real weather API to go further forecasts = { "goa": {"high_c": 32, "low_c": 26, "conditions": "humid, occasional showers"}, "bangalore": {"high_c": 27, "low_c": 18, "conditions": "mild, light evening rain"}, "jaipur": {"high_c": 38, "low_c": 25, "conditions": "hot and dry"}, "manali": {"high_c": 14, "low_c": 3, "conditions": "cold, chance of snow"}, } data = forecasts.get(city.lower(), {"high_c": 28, "low_c": 20, "conditions": "moderate"}) return {"city": city, "days": days, **data} @tool def suggest_packing_list(high_c: int, low_c: int, days: int, conditions: str) -> list: """Suggest what to pack based on temperatures, trip length and conditions. Args: high_c: Daytime high in Celsius low_c: Night-time low in Celsius days: Number of days in the trip conditions: Short description of expected weather """ items = [f"{days + 1} sets of clothes", "toiletries", "phone charger"] if high_c >= 30: items += ["light cotton clothing", "sunscreen", "sunglasses", "reusable water bottle"] if low_c <= 15: items += ["warm jacket", "thermal layer"] elif low_c <= 22: items += ["light jacket for evenings"] if "rain" in conditions.lower() or "shower" in conditions.lower(): items += ["compact umbrella", "quick-dry footwear"] if "snow" in conditions.lower(): items += ["gloves", "woollen cap", "waterproof boots"] return items @tool def estimate_trip_cost(city: str, days: int, travellers: int = 1) -> dict: """Estimate the cost of a trip in Indian rupees. Args: city: Destination city days: Number of days travellers: Number of people travelling (default: 1) """ per_night = {"goa": 3500, "bangalore": 3000, "jaipur": 2500, "manali": 2800} stay = per_night.get(city.lower(), 3000) * days food = 1200 * days * travellers local_travel = 800 * days total = stay + food + local_travel return { "city": city, "days": days, "travellers": travellers, "stay_inr": stay, "food_inr": food, "local_travel_inr": local_travel, "total_inr": total, } model = BedrockModel(model_id="us.anthropic.claude-haiku-4-5-20251001-v1:0") agent = Agent( model=model, tools=[get_weather_forecast, suggest_packing_list, estimate_trip_cost, calculator], system_prompt=( "You are a practical travel assistant. " "When asked about a trip: check the weather first, then suggest what to pack " "based on that weather, then estimate the cost. " "Always say whether the trip fits the user's budget, and keep advice concise." ), ) if __name__ == "__main__": response = agent( "I'm going to Goa for 3 days with 2 friends. My budget is 20000 rupees. " "What should I pack, and does it fit my budget?" ) print(response)
python shivank3/travel_assistant.py # or ask your own question: python shivank3/travel_assistant.py "I'm going to Manali for 4 days, budget 15000"
What to observe together
Notice that the agent calls get_weather_forecast first, then feeds
those numbers into suggest_packing_list, then prices the trip and compares against
the budget. Nobody wrote that sequence — the agentic loop worked it out. That is Diagram 9, running in your
own terminal.
Extension challenges
- Easy — add a
get_visa_requirements(country)tool and ask about an international trip. - Medium — replace the mock weather with a real API call using
requests(this is exactly the pattern from Section 10). - Medium — regroup the three tools into a
TravelToolsclass with shared state, per Section 16.1. - Harder — make the weather and cost lookups
asyncso a multi-city comparison runs in parallel (Section 16.2). - Harder — add
file_writefromstrands_toolsand have the agent save an itinerary to disk.
You have genuinely understood these two modules if you can: (1) explain why the docstring matters, (2) add a fourth tool without help, (3) predict which tools the agent will call for a given question, and (4) debug a retired-model error on your own.
Every command in one place
Copy-paste reference. Run all of these from the project root — the folder containing config.py.
Setup (once)
./setup.sh # macOS / Linux (Windows: setup.bat) source .venv/bin/activate export AWS_DEFAULT_REGION=us-east-1 python 00_check_setup.py # verifies everything, makes a real model call
Every example, in learning order
# ---- Module 1 · first agents ---- python shivank1/01_hello_world_agent.py # simplest agent, no tools python shivank1/02_hello_world_langgraph.py # with a tool — watch the loop # ---- Module 2 · tools ---- python shivank2/01_function_to_tool.py # THE core idea: @tool python shivank2/02_tip_calculator.py # first useful tool agent python shivank2/03_multi_tool_sales.py # 3 tools, agent picks the order python shivank2/04_custom_tool_inventory.py # build your own tool python shivank2/05_prebuilt_tools.py # community tools + system prompt python shivank2/06_multi_prebuilt_tools.py # combining several tools python shivank2/07_use_aws.py # one tool, many AWS services python shivank2/08_class_based_tools.py # shared-resource pattern python shivank2/09_async_tools.py # parallel tools: 2s not 6s # ---- Module 3 · capstone ---- python shivank3/travel_assistant.py
Helpers
python 00_check_setup.py # run whenever something breaks python 01_list_models.py # when a model is retired, pick a new one aws sts get-caller-identity # are my credentials alive? env | grep AWS # what credentials are actually set?
Study plan & troubleshooting
Suggested 3-hour study plan
| Time | Segment | Notes |
|---|---|---|
| 0:00–0:20 | AWS setup (Section 0) | Do not skip. Run python 00_check_setup.py and wait for all six OK lines. |
| 0:20–0:50 | Concepts: RAG → agents → the loop | Diagrams 1, 2, 3, 4 and 9. Keep moving — you will revisit them. |
| 0:50–1:15 | Hello World agents | Both files. Unpack the tool-call output line by line. |
| 1:15–1:25 | Break | |
| 1:25–2:00 | Function → tool → tip calculator | Do the docstring experiment here. |
| 2:00–2:30 | Multi-tool + pre-built + use_aws | Read-only AWS calls only. |
| 2:30–3:00 | Capstone project | Build the skeleton now, finish it in your own time. |
Troubleshooting — keep this open while you work
| Symptom | Cause & fix |
|---|---|
ResourceNotFoundException … end of its life / marked by provider as Legacy
|
Retired model. Run python 01_list_models.py, pick a live one, change MODEL_ID
in config.py. |
AccessDeniedException |
Model not enabled in Bedrock → Model access, or wrong region. Check both. |
InvalidClientTokenId |
Credentials stale or deleted. Create a new access key and re-run aws configure. |
ModuleNotFoundError: No module named 'strands' |
Wrong Python. Activate the venv; in Jupyter, switch the kernel. |
Notebook cell stuck on [*] |
A tool is waiting for consent. Type y at the hidden prompt, or set
BYPASS_TOOL_CONSENT="true" in an earlier cell.
|
Terminal shows quote> or : and seems frozen |
quote> = unclosed quote, press Ctrl+C. : = the AWS CLI pager, press
q. Set export AWS_PAGER="" to stop it.
|
| Agent ignores your tool | Weak docstring or missing type hints. Rewrite the description to say plainly when it should be used. |
ModuleNotFoundError: No module named 'config' |
You ran from inside a subfolder. Run from the project root:
python shivank1/01_hello_world_agent.py.
|
| Worked yesterday, fails today | Almost always expired SSO credentials. Paste a fresh block, then python 00_check_setup.py.
|
The five sentences to remember
- RAG gives a model better information; tools give it the ability to act.
- An agent is a loop: reason → call a tool → read the result → repeat until done.
- A tool is just a Python function plus a decorator, type hints, and a good docstring.
- The docstring is the model's user manual — write it for the model, not for yourself.
- Build small, single-purpose tools; the agent handles the sequencing.