🔗 Multi-Prebuilt Tools#
🔗 Multi-Prebuilt Tools chains two prebuilt strands_tools — http_request and calculator — into a fetch → compute → summarize pipeline, and shows the summary on the card.
How It Works#
The idea behind the demo, the request it sends and the function calls that answer it.
Concept#
Multi-Prebuilt Tools Agent demonstrates how an agent can chain multiple
off-the-shelf tools from strands_tools — specifically
http_request and calculator — to execute an end-to-end live data
analysis pipeline without writing a single line of custom tool code.
When prompted to fetch 5-day stock chart data from Yahoo Finance's JSON API, extract the closing
prices, compute their average, and summarize the trend, the Bedrock agent autonomously calls
http_request(method="GET", url=...) first, parses the nested JSON payload returned
by the server, passes the extracted closing price array into calculator to compute
the exact mean, and writes a clear summary report.
Theory#
1. Cross-Domain Tool Chaining (Network I/O → Computation)
Real-world data workflows rarely stop after a single tool call. Here, two prebuilt tools with completely different responsibilities cooperate through the agent's conversation context:
- Data Acquisition (
http_request): Fetches live external JSON over HTTPS and places the response body into atoolResultblock. - LLM Parsing & Extraction: On the next reasoning turn, the LLM reads the raw JSON structure, locates the closing price array, and constructs a mathematical expression.
- Exact Computation (
calculator): Computes the exact average so the final summary never suffers from LLM floating-point rounding errors.
2. Human-in-the-Loop Tool Consent for Mutating Operations
strands_tools enforces built-in safety boundaries: while http_request
allows read-only GET requests automatically, it prompts for human confirmation on
state-changing HTTP verbs (POST, PUT, PATCH,
DELETE), and tools like file_write require interactive consent unless
BYPASS_TOOL_CONSENT=true is set. In a headless web server with no interactive
terminal, omitting file_write and sticking to read-only GET calls
keeps the server safe from arbitrary file writes or external mutations.
3. Observability Through tool_sequence
By scanning agent.messages for "toolUse" blocks after the run, the
service returns the ordered list of tool calls (http_request(...) followed by
calculator(...)) so you can verify the exact execution trace.
Request flow#
index.html + main.js sends POST /api/multi-prebuilt-tools with { task }.
app.py)
multi_prebuilt_route() calls run_multi_prebuilt_tools_agent(task).
08_multi_prebuilt_tools.py)
Bedrock calls http_request → calculator, then writes the summary.
Code explanation#
1. Core AI & Prebuilt Pipeline Components in 08_multi_prebuilt_tools.py
- Pydantic Contracts (
MultiPrebuiltToolsRequest&MultiPrebuiltToolsResponse): Define the default stock-fetching task (fetching 5-day AAPL chart JSON from Yahoo Finance, extracting closing prices, calculating the average, and summarizing) and the response structure (task: str,response: str, andtool_sequence: list[str]). - Step ② — Model Selection (
model_id): Resolves the target Bedrock model ID, defaulting to"amazon.nova-lite-v1:0". - Prebuilt Tools (
http_request&calculatorfromstrands_tools):http_requestperforms live HTTP/HTTPS requests (executing read-onlyGETcalls automatically) and returns status codes, headers, and response bodies.calculatorevaluates mathematical expressions via SymPy so the arithmetic mean of the extracted closing prices is computed accurately.
run_multi_prebuilt_tools_agent(task: str) -> dict:- Step ③: Creates
Agent(model=BedrockModel(model_id=model_id), tools=[http_request, calculator], system_prompt="You help with data analysis tasks."). - Step ④: Invokes
response = agent(task), letting the agent autonomously chainhttp_request(to fetch the JSON payload) andcalculator(to average the extracted closing prices) before writing the summary. - Step ⑤: Extracts every
"toolUse"block fromagent.messagesin chronological order astool_callsand returns{"task": task, "response": str(response), "tool_sequence": tool_calls}.
- Step ③: Creates
2. End-to-End AI Execution Walkthrough
run_multi_prebuilt_tools_agent(task)initializes a StrandsAgentwithBedrockModel,tools=[http_request, calculator], andsystem_prompt="You help with data analysis tasks.".- On Converse Turn 1, Bedrock inspects the prompt and emits a
toolUseblock forhttp_request(method="GET", url="https://query1.finance.yahoo.com/v8/finance/chart/AAPL?interval=1d&range=5d"). - Strands executes
http_request, fetches the live JSON payload from Yahoo Finance, and appends it as atoolResultblock inagent.messages. - On Converse Turn 2, Bedrock parses the nested JSON in the conversation context, extracts the array of daily closing prices, and emits a second
toolUseblock callingcalculatorwith the arithmetic mean expression. - Strands runs
calculatorvia SymPy and returns the exact average in a secondtoolResult; on Converse Turn 3, Bedrock synthesizes the final summary report and Step ⑤ returns{"task": task, "response": str(response), "tool_sequence": tool_calls}.
Code flow#
POST /api/multi-prebuilt-tools"]:::entry -->|"task"| R["app.py
multi_prebuilt_route()"]:::fn R -->|"task"| S["08_multi_prebuilt_tools.py
run_multi_prebuilt_tools_agent()"]:::fn S -->|"prompt"| A["Strands Agent
agent(prompt)"]:::fn A -->|"messages + tool specs"| B[("Amazon Bedrock
Nova Lite")]:::ext B -->|"toolUse: http_request"| A A -->|"url"| T0["http_request()"]:::fn T0 -->|"response body"| A B -->|"toolUse: calculator"| A A -->|"expression"| T1["calculator()"]:::fn T1 -->|"average"| A A -->|"toolResult"| B B -->|"final text"| A A -->|"reply + agent.messages"| S S -->|"summary + tool_sequence"| R R -->|"JSON"| 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.
08_multi_prebuilt_tools.py#
Multi-Prebuilt Tools Agent Service.
"""Multi-Prebuilt Tools Agent Service.
Demonstrates chaining multiple prebuilt tools in one pipeline:
1. Prebuilt tools from `strands_tools`: `http_request` (fetch) and `calculator` (math).
2. Handles multi-step pipelines: Fetch external API data -> Compute statistics -> Summarize.
3. The practice script also saves the summary with the prebuilt `file_write`; this web demo
returns the summary to the UI instead, so nothing is written on the server.
"""
import os
from dotenv import find_dotenv, load_dotenv
from pydantic import BaseModel
from strands import Agent
from strands.models.bedrock import BedrockModel
from strands_tools import calculator, http_request
# ① Load AWS credentials and Bedrock configuration from .env
load_dotenv(find_dotenv())
class MultiPrebuiltToolsRequest(BaseModel):
"""Input payload for Multi Prebuilt Tools Agent."""
task: str = (
"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 show a short summary"
)
class MultiPrebuiltToolsResponse(BaseModel):
"""Output payload from Multi Prebuilt Tools Agent."""
task: str
response: str
tool_sequence: list[str]
# ② Resolve the target Bedrock model identifier
model_id = os.getenv("BEDROCK_MODEL_ID", "amazon.nova-lite-v1:0")
def run_multi_prebuilt_tools_agent(task: str) -> dict:
"""Run the Multi Prebuilt Tools Agent."""
# ③ Initialize the Strands Agent with the prebuilt http_request and calculator tools
agent = Agent(
model=BedrockModel(model_id=model_id),
tools=[http_request, calculator],
system_prompt="You help with data analysis tasks.",
)
# ④ Execute the multi-step pipeline: fetch HTTP data -> compute average -> summarize
response = agent(task)
# ⑤ Collect the tool calls in the order the agent made them
tool_calls = [
f"{block['toolUse']['name']}({block['toolUse']['input']})"
for message in agent.messages
for block in message["content"]
if "toolUse" in block
]
return {"task": task, "response": str(response), "tool_sequence": tool_calls}
app.py#
Flask Blueprint server and route handler (multi_prebuilt_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 this numbered checklist to review the core concepts, multi-tool chaining patterns, and safety boundaries in the 🔗 Multi-Prebuilt Tools demo:
- Cross-Domain Prebuilt Tool Chaining: By registering
tools=[http_request, calculator]on a single StrandsAgent, you create a complete fetch → parse → compute → summarize pipeline without writing any custom HTTP or math code. - LLM as the Semantic Glue Between Tools:
http_requestreturns raw, deeply nested JSON from Yahoo Finance, whilecalculatorexpects a clean mathematical expression; the LLM acts as the adapter that extracts the closing price numbers from the JSON and formats the formula. - Multi-Turn ReAct Loop Execution: A single
agent(task)call orchestrates three sequential Bedrock Converse turns: Turn 1 callshttp_request, Turn 2 reads the HTTP response and callscalculator, and Turn 3 writes the human-readable summary. - Built-In Tool Consent for Mutating Verbs: In
strands_tools,http_requestexecutes read-onlyGETrequests automatically, whereas state-changing HTTP methods (POST,PUT,DELETE) or tools likefile_writerequire human confirmation. - Server-Safe Web Adaptation: While a local CLI script might include
file_writeto save the summary to disk, this web service omitsfile_writeand returns the summary in the JSON response so unauthenticated web callers cannot write files onto the server container. - Eliminating Arithmetic Drift on Live Data: Even after successfully fetching real-time financial numbers via
http_request, delegating the average calculation tocalculatorguarantees exact decimal arithmetic rather than approximate token prediction. - Execution Trace via
tool_sequence: Iterating throughagent.messagesin order collectstool_sequence = ["http_request(...)", "calculator(...)"], providing verifiable proof of the order in which tools were invoked. - Focused Data-Analysis Persona: Setting
system_prompt="You help with data analysis tasks."primes the model to extract structured metrics from raw API responses and present concise statistical summaries. - Stateless Per-Request Agent Instantiation: Creating a fresh
Agent(...)insiderun_multi_prebuilt_tools_agent()ensures large HTTP response payloads inagent.messagesare garbage-collected after each request rather than accumulating across users.