🧮 Math Assistant#
🧮 Math Assistant uses a prebuilt tool — calculator from the strands_tools package — plus a standing system prompt, so there is no tool code to write at all.
How It Works#
The idea behind the demo, the request it sends and the function calls that answer it.
Concept#
Prebuilt Calculator Agent introduces the strands_tools
ecosystem — a library of battle-tested, off-the-shelf tools maintained alongside the AWS
Strands SDK. By simply writing from strands_tools import calculator and passing
tools=[calculator] to Agent(), the agent gains a full symbolic and
numerical mathematics engine powered by SymPy without writing a single line of
custom tool code.
Paired with a focused system_prompt="You are a helpful math assistant.", the agent
recognizes mathematical expressions, delegates evaluation to the prebuilt
calculator tool, and explains the exact result.
Theory#
1. Why LLMs Struggle with Multi-Digit Math
Transformer language models tokenize numbers into subword chunks and predict the next token via
matrix multiplications and softmax probabilities — they do not run an arithmetic logic unit
(ALU). While an LLM can memorize 12 × 12 = 144, asking it for
429 or the derivative of sin(x2) · ex
by pure token prediction frequently produces plausible-looking wrong digits.
2. Symbolic & Numerical Capabilities of strands_tools.calculator
Unlike a basic four-function eval() wrapper, the prebuilt calculator
tool supports multiple modes via SymPy:
- Evaluate: Exact arithmetic, powers, roots, and trigonometry.
- Solve: Algebraic equations and systems of equations.
- Calculus: Symbolic differentiation (
derive) and integration (integrate). - Matrix Operations: Determinants, inverses, and eigenvalues.
3. Standing Instructions via system_prompt
Passing system_prompt="You are a helpful math assistant." when constructing the
Agent attaches a persistent system instruction to every Bedrock Converse call in
the loop, priming the model to reach for its mathematical toolset and format explanations
clearly.
Request flow#
index.html + main.js sends POST /api/prebuilt-calculator with { query }.
app.py)
prebuilt_calc_route() calls run_prebuilt_calculator_agent(query).
07_prebuilt_tools.py)
Bedrock writes an expression and calls the prebuilt calculator.
Code explanation#
1. Core AI & Prebuilt Tool Components in 07_prebuilt_tools.py
- Pydantic Contracts (
PrebuiltCalculatorRequest&PrebuiltCalculatorResponse): Document the expected input payload (query: strdefaulting to"What's 42 raised to the power of 9?") and the structured output dictionary (query: str,response: str, andtool_calls: list[str]). - Step ② — Model Selection (
model_id): Resolves the target Bedrock model ID, defaulting to"amazon.nova-lite-v1:0". - Prebuilt Tool (
from strands_tools import calculator): Imports the SymPy-backedcalculatortool directly from thestrands_toolspackage — providing symbolic and numerical expression evaluation, equation solving, differentiation, integration, and matrix operations with zero custom@toolcode. run_prebuilt_calculator_agent(query: str) -> dict:- Step ③: Instantiates
Agent(model=BedrockModel(model_id=model_id), tools=[calculator], system_prompt="You are a helpful math assistant.")so the standing system instruction primes Bedrock to delegate arithmetic and symbolic math tocalculator. - Step ④: Calls
response = agent(query), triggering the Strands tool-use loop where Bedrock emits atoolUseblock forcalculator, Strands evaluates the expression via SymPy, and returns the exact result in atoolResultblock. - Step ⑤: Iterates over
agent.messagesand formats every"toolUse"block asf"{block['toolUse']['name']}({block['toolUse']['input']})"intotool_calls, returning{"query": query, "response": str(response), "tool_calls": tool_calls}.
- Step ③: Instantiates
2. End-to-End AI Execution Walkthrough
run_prebuilt_calculator_agent(query)creates aBedrockModeland a StrandsAgentconfigured withtools=[calculator]andsystem_prompt="You are a helpful math assistant.".- Calling
agent(query)(e.g., for"What's 42 raised to the power of 9?") sends the user prompt, the system prompt, and thecalculatorJSON Schema to Amazon Bedrock (amazon.nova-lite-v1:0) via the Converse API. - Bedrock responds with
stopReason="tool_use"and atoolUseblock specifyingname="calculator"and input{"expression": "42**9"}(or equivalent). - The Strands runtime invokes
strands_tools.calculatorlocally, which uses SymPy to compute406,671,383,849,472with arbitrary-precision integer arithmetic and sends it back to Bedrock inside atoolResultmessage. - Bedrock generates the final natural-language answer, and Step ⑤ extracts the
tool_callstrace fromagent.messagesand returns{"query": query, "response": str(response), "tool_calls": tool_calls}.
Code flow#
POST /api/prebuilt-calculator"]:::entry -->|"query"| R["app.py
prebuilt_calc_route()"]:::fn R -->|"query"| S["07_prebuilt_tools.py
run_prebuilt_calculator_agent()"]:::fn S -->|"prompt"| A["Strands Agent
agent(prompt)"]:::fn A -->|"messages + tool specs"| B[("Amazon Bedrock
Nova Lite")]:::ext B -->|"toolUse: calculator"| A A -->|"expression"| T0["calculator()"]:::fn T0 -->|"exact result"| A A -->|"toolResult"| B B -->|"final text"| A A -->|"reply + agent.messages"| S S -->|"reply + tool_calls"| 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.
07_prebuilt_tools.py#
Pre-built Calculator Tool Agent Service.
"""Pre-built Calculator Tool Agent Service.
Demonstrates integrating pre-built community tools with system prompts:
1. Standing System Prompt: "You are a helpful math assistant."
2. Prebuilt `calculator` tool from the `strands_tools` package (no tool code to write).
3. Evaluates complex mathematical queries with precise computation.
"""
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
# ① Load AWS credentials and Bedrock configuration from .env
load_dotenv(find_dotenv())
class PrebuiltCalculatorRequest(BaseModel):
"""Input payload for Prebuilt Calculator Agent."""
query: str = "What's 42 raised to the power of 9?"
class PrebuiltCalculatorResponse(BaseModel):
"""Output payload from Prebuilt Calculator Agent."""
query: str
response: str
tool_calls: list[str]
# ② Resolve the target Bedrock model identifier
model_id = os.getenv("BEDROCK_MODEL_ID", "amazon.nova-lite-v1:0")
def run_prebuilt_calculator_agent(query: str = "What's 42 raised to the power of 9?") -> dict:
"""Run the Prebuilt Calculator Agent."""
# ③ Initialize the Strands Agent with the prebuilt calculator tool and a standing system prompt
agent = Agent(
model=BedrockModel(model_id=model_id),
tools=[calculator],
system_prompt="You are a helpful math assistant.",
)
# ④ Execute the math query
response = agent(query)
# ⑤ Collect every tool call the agent made from its message history
tool_calls = [
f"{block['toolUse']['name']}({block['toolUse']['input']})"
for message in agent.messages
for block in message["content"]
if "toolUse" in block
]
return {"query": query, "response": str(response), "tool_calls": tool_calls}
app.py#
Flask Blueprint server and route handler (prebuilt_calc_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, Strands primitives, and engineering choices in the 🧮 Math Assistant demo:
- Why LLMs Need a Calculator Tool: Autoregressive language models predict subword tokens probabilistically rather than executing hardware ALU instructions, making multi-digit exponentiation (like
429) or symbolic calculus prone to hallucinated digits without an external math engine. - Zero-Boilerplate Prebuilt Tools (
strands_tools): Importingfrom strands_tools import calculatorand passingtools=[calculator]gives an agent production-tested mathematical capabilities without writing a single custom@toolfunction or docstring schema. - SymPy Symbolic & Numerical Engine: Under the hood,
strands_tools.calculatoruses SymPy to support exact arithmetic evaluation, algebraic equation solving, symbolic derivatives and integrals, and matrix operations. - Standing Instructions via
system_prompt: Passingsystem_prompt="You are a helpful math assistant."toAgent(...)attaches a persistent system prompt to every Bedrock Converse call, steering the model to use the calculator tool and explain mathematical results clearly. - Automatic Tool Spec Registration: Even for prebuilt tools imported from external packages,
Agent(tools=[calculator])automatically attaches the tool's name, description, and input JSON Schema (expression,mode, etc.) to the Bedrock Converse request. - Two-Turn Converse Loop: A single call to
agent(query)transparently executes two Bedrock Converse round-trips: Turn 1 translates English into a formalcalculatortool call, and Turn 2 formats the exact SymPy output into a human-friendly response. - Inspecting
agent.messagesfor Observability: Scanningmessage["content"]for"toolUse"blocks afteragent(query)completes yields an exact audit log of the expressions passed tocalculator. - Arbitrary-Precision Accuracy: Because Python and SymPy support arbitrary-precision integers, evaluating
42**9returns the exact 15-digit integer (406671383849472) without floating-point overflow or token-prediction drift. - Dynamic Module Loading in
app.py: Because Python filenames starting with digits (07_prebuilt_tools.py) cannot be imported with standardimportsyntax,app.pyusesimportlib.util.spec_from_file_location()in_load_service()to load each numbered demo cleanly.