🌐 Hello World Agent#
🌐 Hello World Agent is the smallest Strands agent: wrap an Amazon Bedrock model (Nova Lite) in Agent(model=...) and call it with your prompt.
How It Works#
The idea behind the demo, the request it sends and the function calls that answer it.
Concept#
Hello World Strands Agent is the entry point to the AWS Strands Agents
SDK series. It demonstrates the minimal code needed to instantiate an autonomous agent
backed by Amazon Bedrock (defaulting to amazon.nova-lite-v1:0 via
BEDROCK_MODEL_ID) and run a natural-language prompt through its agentic loop.
With no tools attached, agent(prompt) executes a single model turn and returns an
AgentResult object. Crucially, the service creates a fresh Agent(model=...)
instance inside the request handler rather than at module import time, ensuring that concurrent
web users never share or leak conversation state.
Theory#
1. Model vs. Agent in AWS Strands
A raw model client (like boto3.client("bedrock-runtime")) performs a single
stateless text generation call. A Strands Agent wraps a BedrockModel
with three higher-level capabilities that every subsequent demo in this module builds upon:
- Conversation State (
agent.messages): Automatically tracks user, assistant, tool-use, and tool-result blocks across turns. - Tool Registry (
tools=[...]): Inspects decorated Python functions and exposes their schemas to Bedrock's Converse API. - Autonomous Execution Loop: Repeatedly cycles through model inference and local tool execution until the model produces a final answer.
2. Amazon Bedrock Converse API Integration
BedrockModel(model_id=model_id) authenticates using the standard AWS credential
chain (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY,
AWS_DEFAULT_REGION from .env, or an EC2/ECS IAM role) and communicates
through Bedrock's unified Converse API, allowing you to swap between Amazon Nova Lite, Nova Pro,
or Anthropic Claude models by changing only BEDROCK_MODEL_ID.
3. Request-Scoped Agent Instantiation (Multi-Tenant Safety)
Because a Strands Agent appends every prompt and reply to its internal
agent.messages list, a global module-level Agent in a web server would
mix User A's questions into User B's context. Instantiating Agent(...) per request
guarantees clean isolation.
Request flow#
index.html + main.js sends POST /api/hello-world-agent with { prompt }.
app.py)
hello_world_route() calls run_hello_world_agent(prompt).
01_hello_world_agent.py)
Agent(model=BedrockModel(...)) runs agent(prompt).
{ prompt, response }.
Code explanation#
1. Core AI Architecture in 01_hello_world_agent.py
- Pydantic Contracts (
HelloWorldAgentRequest&HelloWorldAgentResponse): Define the structured input/output contract for the agent service:prompt: str(defaulting to"Hello! Tell me a fun fact about AI agents.") and the returnedprompt: strandresponse: strfields. - Step
②Foundation Model Setup (model_id&model): Resolves the target Amazon Bedrock model identifier (checkingBEDROCK_NOVA_LITEandBEDROCK_MODEL_ID, defaulting to"amazon.nova-lite-v1:0") and wraps it in a statelessBedrockModel(model_id=model_id)provider instance. run_hello_world_agent(prompt: str) -> dict: Executes the core Strands agentic loop:- Step
③(agent = Agent(model=model)): Instantiates a fresh StrandsAgentbound to theBedrockModel. Creating a newAgentper call guarantees an isolated conversation state (agent.messages) for every prompt. - Step
④(response = agent(prompt)): Invokes the agent's__call__method, which formats the prompt into a Bedrock Converse user message, sends it to Amazon Nova Lite, and returns anAgentResultobject. Callingstr(response)extracts the assistant's generated text, returning{"prompt": prompt, "response": str(response)}.
- Step
2. End-to-End AI Execution Walkthrough
run_hello_world_agent(prompt)receives the natural-language prompt (e.g.,"Hello! Tell me a fun fact about AI agents.").- It instantiates
Agent(model=model)with no tools registered and invokesagent(prompt). - Strands sends a Converse API request to Amazon Bedrock (
amazon.nova-lite-v1:0) containing the user message. - Because no tools are attached, the foundation model completes its reasoning in a single turn (
stopReason="end_turn") and returns the assistant message. run_hello_world_agent()converts the returnedAgentResultto plain text viastr(response)and returns{"prompt": prompt, "response": str(response)}.
Code flow#
POST /api/hello-world-agent"]:::entry -->|"prompt"| R["app.py
hello_world_route()"]:::fn R -->|"prompt"| S["01_hello_world_agent.py
run_hello_world_agent()"]:::fn S -->|"prompt"| A["Strands Agent
agent(prompt)"]:::fn A -->|"messages"| B[("Amazon Bedrock
Nova Lite")]:::ext B -->|"reply"| A A -->|"AgentResult"| S S -->|"reply text"| R R -->|"JSON { prompt, response }"| UI classDef entry fill:#1e3a5f,stroke:#90caf9,color:#e0e0e0 classDef fn fill:#1e1e1e,stroke:#4caf50,color:#e0e0e0 classDef ext fill:#2d1f3d,stroke:#ce93d8,color:#e0e0e0 classDef ret fill:#1b3320,stroke:#81c784,color:#e0e0e0
Source Code#
Every Python file this demo runs, complete and unedited: the service implementation first, then the Flask Blueprint route handler (app.py) that exposes it.
01_hello_world_agent.py#
AWS Strands Hello World Agent Service.
"""AWS Strands Hello World Agent Service.
Demonstrates the simplest possible autonomous agent:
1. Configures a foundation model (Amazon Nova Lite on Bedrock).
2. Wraps the model in a Strands Agent instance (the agentic loop).
3. Executes a natural language prompt and returns the reply.
"""
import os
from dotenv import find_dotenv, load_dotenv
from pydantic import BaseModel
from strands import Agent
from strands.models.bedrock import BedrockModel
# ① Load AWS credentials and Bedrock configuration from .env
load_dotenv(find_dotenv())
class HelloWorldAgentRequest(BaseModel):
"""Input payload for Hello World Agent."""
prompt: str = "Hello! Tell me a fun fact about AI agents."
class HelloWorldAgentResponse(BaseModel):
"""Output payload from Hello World Agent."""
prompt: str
response: str
# ② Configure the Bedrock foundation model (Amazon Nova Lite is fast and cost-effective)
model_id = os.getenv("BEDROCK_NOVA_LITE", os.getenv("BEDROCK_MODEL_ID", "amazon.nova-lite-v1:0"))
model = BedrockModel(model_id=model_id)
def run_hello_world_agent(prompt: str = "Hello! Tell me a fun fact about AI agents.") -> dict:
"""Execute the Hello World Agent on Amazon Bedrock via Strands."""
# ③ Wrap the foundation model in a Strands Agent instance
agent = Agent(model=model)
# ④ Execute the natural language prompt through the agentic loop
response = agent(prompt)
return {"prompt": prompt, "response": str(response)}
app.py#
Flask Blueprint server and route handler (hello_world_route) in src/python/app.py.
"""Flask server for 05-ai-aws-strands."""
import asyncio
import importlib.util
import os
import sys
from pathlib import Path
from dotenv import find_dotenv, load_dotenv
from flask import Blueprint, Flask, jsonify, request
from flask_cors import CORS
from rate_limiter import check_rate_limit
load_dotenv(find_dotenv())
PATH_PREFIX = os.environ.get("PATH_PREFIX", "")
STATIC_DIR = Path(__file__).resolve().parents[1]
app = Flask(__name__, static_folder=str(STATIC_DIR))
CORS(app)
bp = Blueprint("main", __name__)
_CUR_DIR = Path(__file__).resolve().parent
def _load_service(filename: str, alias: str):
# ① Resolve the numbered service file path and create its import spec
file_path = _CUR_DIR / filename
spec = importlib.util.spec_from_file_location(f"services.{alias}", file_path)
if spec is None or spec.loader is None:
raise ImportError(f"Cannot load module from {file_path}")
# ② Register and execute the module so its functions can be called by routes
mod = importlib.util.module_from_spec(spec)
sys.modules[f"services.{alias}"] = mod
spec.loader.exec_module(mod)
return mod
s_hello = _load_service("01_hello_world_agent.py", "s_hello")
s_langgraph = _load_service("02_hello_world_langgraph.py", "s_langgraph")
s_fn_tool = _load_service("03_function_to_tool.py", "s_fn_tool")
s_tip = _load_service("04_tip_calculator.py", "s_tip")
s_sales = _load_service("05_multi_tool_sales.py", "s_sales")
s_inv = _load_service("06_custom_tool_inventory.py", "s_inv")
s_calc = _load_service("07_prebuilt_tools.py", "s_calc")
s_multi_pre = _load_service("08_multi_prebuilt_tools.py", "s_multi_pre")
s_aws = _load_service("09_use_aws.py", "s_aws")
s_cls_tool = _load_service("10_class_based_tools.py", "s_cls_tool")
s_async_tool = _load_service("11_async_tools.py", "s_async_tool")
s_travel = _load_service("12_travel_assistant.py", "s_travel")
@bp.before_request
def enforce_rate_limit():
# ① Only rate-limit mutating POST requests
if request.method == "POST":
# ② Check whether this client has exceeded 30 requests per hour
blocked, msg, retry_after = check_rate_limit(
request, max_requests=30, window_seconds=3600
)
if blocked:
# ③ Return HTTP 429 with Retry-After header when rate-limited
resp = jsonify({"error": msg})
resp.status_code = 429
resp.headers["Retry-After"] = str(retry_after)
return resp
@bp.route("/")
def index():
# ① Read index.html and inject the runtime PATH_PREFIX into data-api-base
with open(os.path.join(app.static_folder, "index.html"), encoding="utf-8") as f:
html = f.read()
html = html.replace('data-api-base=""', f'data-api-base="{PATH_PREFIX}"')
# ② Return the patched HTML page
return app.response_class(html, mimetype="text/html")
@bp.route("/css/<path:filename>")
def css(filename):
return app.send_static_file(os.path.join("css", filename))
@bp.route("/js/<path:filename>")
def js(filename):
return app.send_static_file(os.path.join("js", filename))
@bp.route("/explanation/<path:filename>")
@bp.route("/info/<path:filename>")
def explanation_pages(filename):
return app.send_static_file(os.path.join("explanation", filename))
@bp.route("/theory/<path:filename>")
def theory_pages(filename):
"""Serve the theory documentation pages from src/theory."""
return app.send_static_file(os.path.join("theory", filename))
@bp.route("/api/hello-world-agent", methods=["POST"])
def hello_world_route():
# ① Parse prompt from JSON payload and reject empty values
data = request.get_json(force=True, silent=True) or {}
prompt = (data.get("prompt") or data.get("message") or "").strip()
if not prompt:
return jsonify({"error": "Prompt cannot be empty"}), 400
try:
# ② Invoke the Strands Hello World Agent in 01_hello_world_agent.py
res = s_hello.run_hello_world_agent(prompt)
res["result"] = res.get("response") or res.get("answer") or ""
return jsonify(res)
except Exception as exc:
return jsonify({"error": str(exc)}), 500
@bp.route("/api/langgraph-agent", methods=["POST"])
def langgraph_route():
# ① Parse prompt from JSON payload and reject empty values
data = request.get_json(force=True, silent=True) or {}
prompt = (data.get("prompt") or data.get("message") or "").strip()
if not prompt:
return jsonify({"error": "Prompt cannot be empty"}), 400
try:
# ② Invoke the LangGraph + Bedrock Converse agent in 02_hello_world_langgraph.py
res = s_langgraph.run_langgraph_agent(prompt)
res["result"] = res.get("response") or res.get("answer") or ""
return jsonify(res)
except Exception as exc:
return jsonify({"error": str(exc)}), 500
@bp.route("/api/function-to-tool", methods=["POST"])
def function_to_tool_route():
# ① Parse question from JSON payload and reject empty values
data = request.get_json(force=True, silent=True) or {}
question = (data.get("question") or data.get("message") or "").strip()
if not question:
return jsonify({"error": "Question cannot be empty"}), 400
try:
# ② Run the HTTP server health-check tool agent in 03_function_to_tool.py
res = s_fn_tool.run_function_to_tool_agent(question)
res["result"] = res.get("response") or res.get("answer") or ""
return jsonify(res)
except Exception as exc:
return jsonify({"error": str(exc)}), 500
@bp.route("/api/tip-calculator", methods=["POST"])
def tip_calculator_route():
# ① Parse query from JSON payload and reject empty values
data = request.get_json(force=True, silent=True) or {}
query = (data.get("query") or data.get("message") or "").strip()
if not query:
return jsonify({"error": "Query cannot be empty"}), 400
try:
# ② Run the multi-parameter tip calculator agent in 04_tip_calculator.py
res = s_tip.run_tip_calculator_agent(query)
res["result"] = res.get("response") or res.get("answer") or ""
return jsonify(res)
except Exception as exc:
return jsonify({"error": str(exc)}), 500
@bp.route("/api/multi-tool-sales", methods=["POST"])
def multi_tool_sales_route():
# ① Parse task from JSON payload and reject empty values
data = request.get_json(force=True, silent=True) or {}
task = (data.get("task") or data.get("message") or "").strip()
if not task:
return jsonify({"error": "Task cannot be empty"}), 400
try:
# ② Run the 3-tool sales pipeline agent in 05_multi_tool_sales.py
res = s_sales.run_multi_tool_sales_agent(task)
res["result"] = res.get("response") or res.get("answer") or ""
return jsonify(res)
except Exception as exc:
return jsonify({"error": str(exc)}), 500
@bp.route("/api/custom-inventory", methods=["POST"])
def custom_inventory_route():
# ① Parse question from JSON payload and reject empty values
data = request.get_json(force=True, silent=True) or {}
question = (data.get("question") or data.get("message") or "").strip()
if not question:
return jsonify({"error": "Question cannot be empty"}), 400
try:
# ② Run the custom inventory lookup agent in 06_custom_tool_inventory.py
res = s_inv.run_custom_inventory_agent(question)
res["result"] = res.get("response") or res.get("answer") or ""
return jsonify(res)
except Exception as exc:
return jsonify({"error": str(exc)}), 500
@bp.route("/api/prebuilt-calculator", methods=["POST"])
def prebuilt_calculator_route():
# ① Parse math query from JSON payload and reject empty values
data = request.get_json(force=True, silent=True) or {}
query = (data.get("query") or data.get("message") or "").strip()
if not query:
return jsonify({"error": "Query cannot be empty"}), 400
try:
# ② Run the prebuilt calculator tool agent in 07_prebuilt_tools.py
res = s_calc.run_prebuilt_calculator_agent(query)
res["result"] = res.get("response") or res.get("answer") or ""
return jsonify(res)
except Exception as exc:
return jsonify({"error": str(exc)}), 500
@bp.route("/api/multi-prebuilt-tools", methods=["POST"])
def multi_prebuilt_tools_route():
# ① Parse task from JSON payload and reject empty values
data = request.get_json(force=True, silent=True) or {}
task = (data.get("task") or data.get("message") or "").strip()
if not task:
return jsonify({"error": "Task cannot be empty"}), 400
try:
# ② Run the http_request + calculator agent in 08_multi_prebuilt_tools.py
res = s_multi_pre.run_multi_prebuilt_tools_agent(task)
res["result"] = res.get("response") or res.get("answer") or ""
return jsonify(res)
except Exception as exc:
return jsonify({"error": str(exc)}), 500
@bp.route("/api/use-aws", methods=["POST"])
def use_aws_route():
# ① Parse natural-language AWS prompt from JSON payload and reject empty values
data = request.get_json(force=True, silent=True) or {}
prompt = (data.get("prompt") or data.get("message") or "").strip()
if not prompt:
return jsonify({"error": "Prompt cannot be empty"}), 400
try:
# ② Run the prebuilt use_aws (boto3) agent in 09_use_aws.py
res = s_aws.run_use_aws_agent(prompt)
res["result"] = res.get("response") or res.get("answer") or ""
return jsonify(res)
except Exception as exc:
return jsonify({"error": str(exc)}), 500
@bp.route("/api/class-based-tools", methods=["POST"])
def class_based_tools_route():
# ① Parse query from JSON payload and reject empty values
data = request.get_json(force=True, silent=True) or {}
query = (data.get("query") or data.get("message") or "").strip()
if not query:
return jsonify({"error": "Query cannot be empty"}), 400
try:
# ② Run the stateful InventoryTools class agent in 10_class_based_tools.py
res = s_cls_tool.run_class_based_tools_agent(query)
res["result"] = res.get("response") or res.get("answer") or ""
return jsonify(res)
except Exception as exc:
return jsonify({"error": str(exc)}), 500
@bp.route("/api/async-tools", methods=["POST"])
def async_tools_route():
# ① Parse query from JSON payload and reject empty values
data = request.get_json(force=True, silent=True) or {}
query = (data.get("query") or data.get("message") or "").strip()
if not query:
return jsonify({"error": "Query cannot be empty"}), 400
try:
# ② Run the async parallel warehouse tools agent in 11_async_tools.py
res = asyncio.run(s_async_tool.run_async_tools_agent(query))
res["result"] = res.get("response") or res.get("answer") or ""
return jsonify(res)
except Exception as exc:
return jsonify({"error": str(exc)}), 500
@bp.route("/api/travel-assistant", methods=["POST"])
def travel_assistant_route():
# ① Parse travel query from JSON payload and reject empty values
data = request.get_json(force=True, silent=True) or {}
query = (data.get("query") or data.get("message") or "").strip()
if not query:
return jsonify({"error": "Query cannot be empty"}), 400
try:
# ② Run the 4-tool travel assistant agent in 12_travel_assistant.py
res = s_travel.run_travel_assistant_agent(query)
res["result"] = res.get("response") or res.get("answer") or ""
return jsonify(res)
except Exception as exc:
return jsonify({"error": str(exc)}), 500
app.register_blueprint(bp, url_prefix=PATH_PREFIX)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
Quick Revision#
Key AI concepts, architectural patterns, and practical takeaways from this demo.
Quick revision#
Use these core takeaways to review how AWS Strands initializes and runs a foundational Bedrock agent:
- Two-Object Architecture (
BedrockModel+Agent): In AWS Strands,BedrockModelencapsulates the stateless foundation model connection and inference parameters, whileAgentprovides the stateful conversation history (agent.messages), tool registry, and autonomous execution loop. - Default Foundation Model (
amazon.nova-lite-v1:0): The service resolvesBEDROCK_NOVA_LITEorBEDROCK_MODEL_IDfrom.envand defaults to Amazon Nova Lite — a low-latency, cost-efficient multimodal model ideal for interactive agent loops. - Unified Bedrock Converse API: Under the hood,
BedrockModelcommunicates through Amazon Bedrock's Converse API, meaning you can switch between Amazon Nova, Anthropic Claude, or Meta Llama models by changing only the model ID string. - Zero-Tool Single-Turn Execution: When
Agent(model=model)is initialized without atools=[...]list, callingagent(prompt)executes a single Converse turn and returns anAgentResultobject that stringifies cleanly viastr(response). - Request-Scoped Agent Instantiation: Because a Strands
Agentappends every user prompt and assistant response toagent.messages,run_hello_world_agent()creates a newAgent(model=model)on each request while reusing the statelessBedrockModel, preventing cross-user context leakage in a multi-tenant web server. - Standard AWS Credential Chain: Calling
load_dotenv(find_dotenv())populates environment variables thatboto3automatically discovers alongside IAM roles or AWS CLI profiles. - Dynamic Module Loading for Numbered Scripts: In
app.py,_load_service()usesimportlib.util.spec_from_file_locationso learning scripts can keep their ordered filenames (01_hello_world_agent.py) while still being imported cleanly into Flask routes. - Pydantic Contract Documentation: Defining
HelloWorldAgentRequestandHelloWorldAgentResponsemakes the expected JSON payload and response shape explicit and self-documenting.