Build an AI Chatbot with Amazon Bedrock
Build a Python chatbot that calls Amazon Bedrock's Converse API with tool use.
Introduction
30 Second Summary
Every time you ask a virtual assistant for help, there's an AI model behind the scenes generating a response. But how does your message actually reach the model, and how does the model remember what you said five messages ago?
In this project, you will build a terminal-based AI chatbot powered by Amazon Bedrock. You'll make API calls to a foundation model, teach your chatbot to remember conversation history, and give it the ability to call real functions.
What You'll Build
A Python chatbot that talks to Amazon Bedrock's Converse API, maintains multi-turn conversations, and uses tool calling to answer questions with real-world data.
By the end of this project, you'll have:
- A working Python script that makes your first Bedrock API call and gets an AI-generated response.
- A multi-turn chatbot that remembers context across an entire conversation.
- A tool-equipped chatbot that can call external functions (like a weather lookup) when it needs real-world data.
- Secret Mission: swap out the AI model for a different provider and compare responses without changing your conversation logic.
Are there any prerequisites?
You'll need basic Python knowledge (variables, functions, if-statements) and an AWS account. If you don't have an AWS account yet, you'll create one in Step 1.
Set Up Your AWS Environment
Before you can talk to AI models through Amazon Bedrock, you need the right tools on your machine.
In this step, you will install Python and the AWS CLI, configure your credentials, and enable access to the model your chatbot will use.
In this step, get ready to:
- Install Python and the AWS CLI.
- Configure AWS credentials for your account.
- Enable model access for Amazon Nova Micro in the Bedrock console.
Install Python and the AWS CLI
Python is the programming language you will use to write your chatbot script. The AWS CLI is a command-line tool that lets you interact with AWS services from your terminal.
Let's check if Python 3.12 or higher is already installed on your machine.
- Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or cmd (Windows) and press Enter to open it.
- Check your Python version by running:
python3 --version
✔️ I see version 3.12 or higher
Python is ready to go. Move on to checking the AWS CLI below.
ⓧ I see a version lower than 3.12
You have an older version of Python installed. You need 3.12 or higher for this project.
- Go to python.org/downloads and download the latest Python 3 installer for your operating system.
- Run the installer. On Windows, make sure to check the Add python.exe to PATH checkbox before clicking Install Now.
- Close and reopen your terminal, then run python3 --version again to confirm.
macOS users with an older system Python
macOS may ship with an older Python version. After installing a newer version from python.org, you may need to use python3.12 or python3.13 explicitly, or use a version manager like pyenv to set the default.
ⓧ Command not found
Python is not installed on your machine yet.
- Go to python.org/downloads and download the latest Python 3 installer for your operating system.
- Run the installer. On Windows, make sure to check the Add python.exe to PATH checkbox before clicking Install Now.
- Close and reopen your terminal, then run python3 --version to confirm.
Windows users
On Windows, you may need to use python --version instead of python3 --version. Both are fine as long as the version is 3.12 or higher.
Now let's check the AWS CLI. Run:
aws --version
✔️ I see aws-cli/2.x.x
The AWS CLI v2 is installed. You are ready to configure it.
ⓧ I see aws-cli/1.x.x
You have version 1 installed. This project requires version 2. Follow the upgrade instructions below to install version 2.
- On macOS, download and install the official package from https://awscli.amazonaws.com/AWSCLIV2.pkg.
- On Windows, download and run the MSI installer from https://awscli.amazonaws.com/AWSCLIV2.msi.
- Close and reopen your terminal, then run aws --version to confirm it shows aws-cli/2.x.x.
ⓧ Command not found
The AWS CLI is not installed yet.
- On macOS, download and install the official package from https://awscli.amazonaws.com/AWSCLIV2.pkg.
- On Windows, download and run the MSI installer from https://awscli.amazonaws.com/AWSCLIV2.msi.
- Follow the installer prompts to complete the installation.
- Close and reopen your terminal, then run aws --version to confirm it shows aws-cli/2.x.x.
What is the AWS CLI?
The AWS CLI is a command-line tool that lets you manage AWS services directly from your terminal. Instead of clicking through the AWS console, you can run commands to create resources, configure settings, and interact with services like Amazon Bedrock.
Still having trouble installing?
Make sure you close and reopen your terminal after installation. If the commands still aren't found, check that the install location was added to your system PATH.
help me install Python and the AWS CLI on my machine
Configure AWS credentials
The AWS CLI needs to know who you are before it can call AWS services on your behalf. You will configure it with an access key, secret key, and default region.
If you do not have an AWS access key yet, create one in the AWS console under IAM > Users > your user > Security credentials > Create access key.
- Configure the AWS CLI by running:
aws configure
- When prompted, enter your AWS Access Key ID and press Enter.
- Enter your AWS Secret Access Key and press Enter.
- For Default region name, type us-east-1 and press Enter.
- For Default output format, press Enter to accept the default (json).
Why us-east-1?
AWS recommends US East (N. Virginia) us-east-1 as the region for getting started with Amazon Bedrock. It has the broadest model availability.
Now install the boto3 library, which is the Python SDK for AWS.
- Install boto3 by running:
pip install boto3
What is boto3?
boto3 is the official AWS SDK for Python. It lets your Python scripts create, configure, and manage AWS services. Your chatbot script will use boto3 to call the Bedrock API.
- Verify the installation worked by running:
python3 -c "import boto3; print(boto3.__version__)"
You should see a version number printed in the terminal.
pip command not found?
On some systems, you may need to use pip3 instead of pip. Try running pip3 install boto3 instead.
help me install boto3 with pip
Enable model access in Amazon Bedrock
Amazon Bedrock gives you access to foundation models from multiple providers, but you need to explicitly enable the models you want to use. You will enable Amazon Nova Micro, a fast and cost-effective text model.
- Open the Amazon Bedrock console by going to https://console.aws.amazon.com/bedrock in your browser.
- Make sure US East (N. Virginia) is selected as your region in the top-right corner of the console.
- In the left sidebar, click Model access.
- Click Modify model access.
- Find Amazon Nova Micro in the list and check the box next to it.
- Click Next, then Submit.
Access is typically approved instantly for Amazon's own models. You should see the status change to Access granted within a few seconds.
What is Amazon Bedrock?
Amazon Bedrock is a fully managed service that provides secure, enterprise-grade access to high-performing foundation models from leading AI companies. You can access models from Amazon, Anthropic, Meta, and others through a single API.
The model you just enabled, Amazon Nova Micro (amazon.nova-micro-v1:0), is Amazon's fastest and cheapest text model. At $0.035 per 1M input tokens and $0.14 per 1M output tokens, this entire project will cost less than $0.01.
Don't see Model access in the sidebar?
Double-check that your region is set to US East (N. Virginia) in the top-right corner. Bedrock features vary by region.
help me find model access in the Bedrock console
Your environment is ready. Next up, you will write your first Python script and make your first API call to Amazon Bedrock.
Make Your First Bedrock API Call
Your environment is ready to go. You have Python, the AWS CLI, and boto3 installed, and Amazon Bedrock has access to the Amazon Nova Micro model.
Now it's time to actually talk to an AI model. In this step, you'll write a Python script that sends a message to Amazon Nova Micro through the Converse API and prints the response in your terminal.
In this step, get ready to:
- Create a Python script that calls the Bedrock Converse API.
- Explore the response object to understand its structure.
- Customize the model's behavior with inference parameters and a system prompt.
Create your first script
Every interaction with a foundation model on Bedrock starts with creating a client and calling an API. You'll use the Converse API because it works the same way across all Bedrock models.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- Open a terminal inside VS Code by pressing Ctrl+` (backtick key, top-left of your keyboard).
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir bedrock-chatbot
cd bedrock-chatbot
- Create the script file by running:
touch chatbot.py
- Confirm the file exists by running:
ls
You should see chatbot.py in the output.
- Open chatbot.py in the VS Code editor by clicking on it in the Explorer sidebar, or by running:
code chatbot.py
- Paste the following code into chatbot.py:
import boto3
import json
from botocore.exceptions import ClientError
# Create the Bedrock Runtime client
client = boto3.client("bedrock-runtime", region_name="us-east-1")
model_id = "amazon.nova-micro-v1:0"
# Send a single message to the model
response = client.converse(
modelId=model_id,
messages=[{"role": "user", "content": [{"text": "Hello! What can you help me with?"}]}],
)
# Print the model's response
print(response["output"]["message"]["content"][0]["text"])
What does this code do?
This script connects to Amazon Bedrock and sends a single message to the AI model.
- boto3.client("bedrock-runtime") creates a connection to the Bedrock Runtime service in us-east-1.
- model_id tells Bedrock which foundation model to use. amazon.nova-micro-v1:0 is Amazon's fastest and cheapest text model.
- client.converse() sends your message and returns the model's full response as a dictionary.
- The messages list contains one user message. The model reads it and generates a reply.
- Save chatbot.py by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Run the script in your terminal:
python chatbot.py
You should see a text response from the AI model printed in your terminal. The exact wording varies each time, but it should be a friendly reply about what the assistant can help with.
Not seeing a response?
- If you see AccessDeniedException, your AWS credentials may not have permission to call Bedrock. Verify your IAM user has the bedrock:InvokeModel permission.
- If you see a model-not-found error, double check that you enabled access to Amazon Nova Micro in the Bedrock console.
- If you see ModuleNotFoundError: No module named 'boto3', run pip install boto3 again in the same terminal.
Still stuck? Help me debug my first Bedrock API call
Understand the response structure
The Converse API returns a dictionary with several useful fields. The generated text is nested deep inside the response object. The API also tracks how many tokens your request consumed.
What is a token?
A token is a chunk of text that the model processes. It can be a whole word, part of a word, or punctuation. Amazon Nova Micro charges based on the number of input tokens (what you send) and output tokens (what the model generates).
For this project, you'll use fewer than 10,000 tokens total. That costs well under $0.01.
Here's how the response is structured:
- response["output"]["message"]["content"][0]["text"] contains the AI's generated reply as a string.
- response["usage"] contains a dictionary showing inputTokens and outputTokens counts.
Let's add a line that prints the token usage so you can see exactly how many tokens each call consumes.
- In chatbot.py, find the last line of your script:
print(response["output"]["message"]["content"][0]["text"])
- Add this line directly below it:
print("Tokens used:", response["usage"])
- Save chatbot.py and run the script again:
python chatbot.py
You should see the AI's text response followed by a line showing the token counts, like Tokens used: {'inputTokens': 12, 'outputTokens': 45}. Your exact numbers will differ.
Why track token usage?
Token counts tell you exactly how much each API call costs. Amazon Nova Micro charges $0.035 per 1 million input tokens and $0.14 per 1 million output tokens. Monitoring usage helps you estimate costs as you build more complex applications.
Experiment with inference parameters
Right now, the model responds with its default behavior. You can customize how it generates text by adding inference parameters. You can also give it a personality with a system prompt.
What are inference parameters?
Inference parameters control how the model picks its next word.
- temperature controls randomness. Lower values (like 0.2) make the model more predictable. Higher values (like 0.9) make it more creative.
- maxTokens sets the maximum length of the response. The model stops generating once it hits this limit.
- topP controls diversity by limiting which words the model considers. A value of 0.9 means it only picks from the top 90% most likely words.
- In chatbot.py, find the line that sets model_id:
model_id = "amazon.nova-micro-v1:0"
- Add this system prompt directly below it (leave one blank line between):
# Give the model a personality
system = [{"text": "You are a helpful weather assistant. You can check the weather for users. Be concise and friendly."}]
- Now find your client.converse() call. It currently looks like this:
response = client.converse(
modelId=model_id,
messages=[{"role": "user", "content": [{"text": "Hello! What can you help me with?"}]}],
)
- Replace the entire client.converse() call with this updated version that includes the system prompt and inference parameters:
# Call the Converse API with system prompt and inference parameters
response = client.converse(
modelId=model_id,
messages=[{"role": "user", "content": [{"text": "Hello! What can you help me with?"}]}],
system=system,
inferenceConfig={"maxTokens": 512, "temperature": 0.5, "topP": 0.9},
)
What changed in this call?
- system=system passes the system prompt. This tells the model to behave as a weather assistant before it reads the user's message.
- inferenceConfig sets the generation parameters. A temperature of 0.5 keeps responses focused but not robotic. A maxTokens of 512 keeps replies concise.
- Save chatbot.py and run the script again:
python chatbot.py
This time, the response should mention weather-related topics. The system prompt shaped the model's personality, so it now identifies itself as a weather assistant.
Response doesn't mention weather?
- Make sure the system variable is defined above the client.converse() call. Python reads top to bottom.
- Check that you included system=system inside the converse() call, not outside it.
- Verify the system value is a list containing a dictionary with a "text" key, not a plain string.
Still stuck? Help me fix my system prompt in Amazon Bedrock
Try different temperatures
Run the script a few times. Then try changing temperature to 0.1 (very predictable) and 0.9 (very creative) to see how it affects the replies. Change it back to 0.5 when you're done experimenting.
✔️ Awesome, I've got everything!
Great. Make sure you've saved chatbot.py with all the changes from this step.
ⓧ I'd like to double check the full code
Here's the complete chatbot.py at the end of this step:
import boto3
import json
from botocore.exceptions import ClientError
client = boto3.client("bedrock-runtime", region_name="us-east-1")
model_id = "amazon.nova-micro-v1:0"
system = [{"text": "You are a helpful weather assistant. You can check the weather for users. Be concise and friendly."}]
response = client.converse(
modelId=model_id,
messages=[{"role": "user", "content": [{"text": "Hello! What can you help me with?"}]}],
system=system,
inferenceConfig={"maxTokens": 512, "temperature": 0.5, "topP": 0.9},
)
print(response["output"]["message"]["content"][0]["text"])
print("Tokens used:", response["usage"])
You just made your first AI model respond to a message through code. Next up, you'll turn this single call into a real conversation where the model remembers everything you've said.
Build a Multi-Turn Conversation
In the previous step, you sent a single message to Amazon Bedrock and got a response back. That's great for a one-off question, but real conversations require memory.
The Converse API is stateless. Every time you call it, the model has zero memory of what happened before. To build a multi-turn chatbot, your code needs to store every message and resend the full conversation history with each new request.
In this step, get ready to:
- Add a conversation history list that accumulates messages across turns.
- Create an interactive terminal loop that reads user input and prints responses.
- Test that the model remembers earlier parts of the conversation.
Add conversation history
Right now your script sends a hardcoded message and exits. You need to replace that single-shot call with a conversation list and a chat() function that appends each message to the list before calling the API.
- In chatbot.py, find and delete everything from the response = client.converse( line to the end of the file (the two print statements).
- Also remove the "topP": 0.9 parameter reference you had in the old code.
- Replace all of that deleted code with the following:
conversation = []
def chat(user_input):
conversation.append({
"role": "user",
"content": [{"text": user_input}]
})
try:
response = client.converse(
modelId=model_id,
messages=conversation,
system=system,
inferenceConfig={"maxTokens": 512, "temperature": 0.5},
)
assistant_message = response["output"]["message"]
conversation.append(assistant_message)
return assistant_message["content"][0]["text"]
except ClientError as e:
return f"ERROR: {e}"
What does this code do?
This code creates the core conversation engine for your chatbot.
- conversation = [] creates an empty list that will hold every message exchanged between you and the model.
- Each time chat() is called, it appends the user's message to the list, sends the entire list to the Converse API, then appends the assistant's response to the same list.
- Because messages=conversation sends the full history every time, the model can see all prior context when generating a new reply.
Why is the model stateless?
Amazon Bedrock does not store any text you provide between API calls. Each call is independent. Your code is responsible for maintaining conversation state by resending all previous messages with every new request.
- Save chatbot.py.
You won't see a change yet because the script has no way to run interactively. The next chunk adds the main loop.
Seeing a syntax error on save?
Make sure you deleted the old response = client.converse(...) block completely. Leftover lines from the previous step cause indentation or name errors.
help me fix a syntax error in my chatbot.py after replacing the single-shot Converse call with a chat function
Create an interactive loop
Your chat() function handles a single turn. Now you need a loop that repeatedly reads input from the terminal, passes it to chat(), and prints the response.
- Add the following code at the bottom of chatbot.py, below the chat() function:
def main():
print("Weather Chatbot (powered by Amazon Bedrock)")
print("Type 'quit' to exit.\n")
while True:
user_input = input("You: ").strip()
if user_input.lower() in ("quit", "exit"):
print("Goodbye!")
break
if not user_input:
continue
response = chat(user_input)
print(f"Bot: {response}\n")
if __name__ == "__main__":
main()
What does this code do?
- main() prints a welcome banner, then enters a while True loop that waits for terminal input on each iteration.
- If the user types quit or exit, the loop breaks and the script ends.
- if __name__ == "__main__" ensures main() only runs when you execute the file directly, not when it is imported as a module.
- Save chatbot.py.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before running it in the next substep.
ⓧ I'd like to double check the full code
Here is the complete chatbot.py at this point:
import boto3
import json
from botocore.exceptions import ClientError
client = boto3.client("bedrock-runtime", region_name="us-east-1")
model_id = "amazon.nova-micro-v1:0"
system = [{"text": "You are a helpful weather assistant. You can check the weather for users. Be concise and friendly."}]
conversation = []
def chat(user_input):
conversation.append({
"role": "user",
"content": [{"text": user_input}]
})
try:
response = client.converse(
modelId=model_id,
messages=conversation,
system=system,
inferenceConfig={"maxTokens": 512, "temperature": 0.5},
)
assistant_message = response["output"]["message"]
conversation.append(assistant_message)
return assistant_message["content"][0]["text"]
except ClientError as e:
return f"ERROR: {e}"
def main():
print("Weather Chatbot (powered by Amazon Bedrock)")
print("Type 'quit' to exit.\n")
while True:
user_input = input("You: ").strip()
if user_input.lower() in ("quit", "exit"):
print("Goodbye!")
break
if not user_input:
continue
response = chat(user_input)
print(f"Bot: {response}\n")
if __name__ == "__main__":
main()
Script exits immediately without prompting?
Make sure the if __name__ == "__main__": line is at the very bottom of the file, outside any function. If it is indented inside chat() or main(), Python won't call main() when you run the script.
help me figure out why my chatbot script exits without showing a prompt
Test context retention
The whole point of conversation history is that the model can reference earlier turns. Let's prove it works by having a multi-turn conversation and asking the model to recall something you said earlier.
- Run your chatbot by executing this command in the terminal:
python chatbot.py
- When you see the You: prompt, type a greeting like Hi, my name is Alex and press Enter.
- Ask a follow-up question like What's my name? to test whether the model remembers your earlier message.
- Type quit to exit when you're done.
What should I see?
The model should correctly recall your name from the earlier turn. This proves your conversation list is working. Each call to the Converse API includes the full message history, giving the model context it would otherwise not have.
Model doesn't remember earlier messages?
Check that conversation.append(assistant_message) appears after each successful API call. If the assistant's response isn't added to the list, only user messages get sent on the next turn.
Also confirm you're passing messages=conversation (the growing list) rather than a new list with only the latest message.
help me debug why my Bedrock chatbot doesn't remember earlier messages in the conversation
Your chatbot now holds a real conversation. Next up, you'll give it superpowers by connecting it to a tool so it can look up real-world data.
Add Tool Use (Function Calling)
Your chatbot can hold a conversation and remember what you've discussed. But right now, it can only talk. It cannot actually do anything in the real world.
In this step, you'll give your chatbot a superpower: the ability to call a Python function when it needs real data. When a user asks about the weather, the model will decide on its own to use a tool, run your function, and weave the result into a natural response.
In this step, get ready to:
- Define a weather tool and describe it to the model.
- Handle the tool use loop so the model can call your function and get results back.
- Test end-to-end by asking your chatbot a weather question.
Define a tool
In Amazon Bedrock, the model does not call tools directly. You supply a tool definition alongside your messages, and the model decides whether to request that tool. Your code then runs the function and sends the result back.
You need two things: a Python function that actually does the work, and a JSON schema that tells the model what the function does and what inputs it expects.
- In chatbot.py, find the line that says client = boto3.client("bedrock-runtime", region_name="us-east-1").
- Add the following get_weather function directly above that line:
# --- Tool Definition ---
def get_weather(city):
"""Returns hardcoded weather data for demonstration."""
weather_data = {
"new york": {"temperature": "52°F", "condition": "Partly cloudy", "humidity": "65%"},
"london": {"temperature": "48°F", "condition": "Rainy", "humidity": "80%"},
"tokyo": {"temperature": "68°F", "condition": "Sunny", "humidity": "45%"},
"sydney": {"temperature": "75°F", "condition": "Clear", "humidity": "55%"},
}
city_lower = city.lower()
if city_lower in weather_data:
return json.dumps(weather_data[city_lower])
return json.dumps({"error": f"Weather data not available for {city}"})
# --- Bedrock Client Setup ---
What does this code do?
This function simulates a real weather API. It stores hardcoded data for four cities and returns it as a JSON string.
The city.lower() call normalizes the input so "New York" and "new york" both match. If the city is not found, it returns an error message instead.
- Now find the line that says conversation = [].
- Add the following tool configuration and function mapping directly above that line:
# --- Tool Configuration ---
tool_config = {
"tools": [
{
"toolSpec": {
"name": "get_weather",
"description": "Gets the current weather for a given city.",
"inputSchema": {
"json": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The name of the city to get weather for."
}
},
"required": ["city"]
}
}
}
}
]
}
# --- Map tool names to functions ---
tool_functions = {
"get_weather": get_weather
}
# --- Conversation History ---
What does this code do?
The tool_config dictionary describes your tool to the model using Bedrock's toolSpec format. It includes the function name, a description of what it does, and an inputSchema that defines which parameters the function accepts.
The tool_functions dictionary maps tool names (strings) to actual Python functions. When the model requests a tool by name, your code looks up the matching function here and calls it.
- Save chatbot.py.
Seeing a syntax error?
Make sure tool_config is placed between your system prompt and the conversation = [] line. The get_weather function must be above tool_functions since the dictionary references it.
help me fix the placement of my tool definition in chatbot.py
Handle the tool use loop
When you pass toolConfig to the Converse API, the model can respond with a stopReason of "tool_use" instead of the usual text. This means it wants you to run a function and send the result back before it finishes answering.
- In chatbot.py, find the existing chat function (starting with def chat(user_input):).
- Replace the entire chat function with the following updated version:
def chat(user_input):
"""Send a message and handle tool use if requested."""
# Add user message to history
conversation.append({
"role": "user",
"content": [{"text": user_input}]
})
try:
# Call Bedrock Converse API
response = client.converse(
modelId=model_id,
messages=conversation,
system=system,
inferenceConfig={"maxTokens": 512, "temperature": 0.5},
toolConfig=tool_config,
)
# Get the assistant message and stop reason
assistant_message = response["output"]["message"]
stop_reason = response["stopReason"]
# Add assistant response to history
conversation.append(assistant_message)
# Check if the model wants to use a tool
if stop_reason == "tool_use":
# Find the toolUse block
tool_use_block = None
for block in assistant_message["content"]:
if "toolUse" in block:
tool_use_block = block["toolUse"]
break
if tool_use_block:
tool_name = tool_use_block["name"]
tool_input = tool_use_block["input"]
tool_use_id = tool_use_block["toolUseId"]
print(f" [Calling tool: {tool_name}({tool_input})]")
# Run the tool
result = tool_functions[tool_name](**tool_input)
# Send tool result back to the model
conversation.append({
"role": "user",
"content": [
{
"toolResult": {
"toolUseId": tool_use_id,
"content": [{"text": result}]
}
}
]
})
# Get the final response
follow_up = client.converse(
modelId=model_id,
messages=conversation,
system=system,
inferenceConfig={"maxTokens": 512, "temperature": 0.5},
toolConfig=tool_config,
)
final_message = follow_up["output"]["message"]
conversation.append(final_message)
return final_message["content"][0]["text"]
# No tool use - return the text directly
return assistant_message["content"][0]["text"]
except ClientError as e:
return f"ERROR: {e}"
What does this code do?
This updated function handles the complete tool use loop:
- It passes toolConfig=tool_config to the Converse API so the model knows which tools are available.
- It checks response["stopReason"] to see if the model stopped because it wants to use a tool.
- If stopReason is "tool_use", it extracts the tool name, input, and toolUseId from the response.
- It runs the matching Python function and appends the result as a toolResult message.
- It makes a second API call so the model can produce a final human-readable answer that incorporates the tool result.
Why two API calls?
Bedrock models are stateless. The first call returns a tool request instead of text. Your code runs the function locally, then sends the result back in a second call. The model uses that result to compose its final natural language response.
- Save chatbot.py.
Seeing an indentation error?
Make sure the entire chat function is indented consistently with spaces. If you mixed the old function with the new code, you may have leftover lines causing a conflict. Delete the old chat function completely before pasting the new one.
help me fix the indentation in my updated chat function
Test end-to-end
Everything is wired up. Time to ask your chatbot a weather question and watch the full tool use loop in action.
- Run your chatbot by executing this command:
python chatbot.py
- When you see the You: prompt, type:
What's the weather in New York?
You should see output similar to this:
You: What's the weather in New York?
[Calling tool: get_weather({'city': 'New York'})]
Bot: The weather in New York is 52°F and partly cloudy with 65% humidity.
What just happened?
The model read your question, recognized it needed weather data, and requested the get_weather tool with city set to "New York". Your code ran the function, sent the JSON result back, and the model composed a friendly answer from the raw data.
- Try a few more questions to see how the model handles different scenarios:
- Ask about a supported city like "Tokyo" or "London".
- Ask about an unsupported city like "Paris" to see the error handling.
- Ask a non-weather question like "What is Python?" to confirm the model skips the tool when it is not needed.
- Type quit to exit when you are done.
Getting an error when asking about weather?
If you see a ClientError, double-check that you enabled Amazon Nova Micro in the Bedrock console (Step 1). If you see a KeyError, make sure tool_functions contains "get_weather": get_weather and that the name in your toolSpec matches exactly.
help me debug why my chatbot tool use is failing
✔️ Awesome, I've got everything!
Great. Double check you have saved chatbot.py and confirmed the tool use loop works.
ⓧ I'd like to double check the full code
Here is the complete chatbot.py at this point:
import boto3
import json
from botocore.exceptions import ClientError
# --- Tool Definition ---
def get_weather(city):
"""Returns hardcoded weather data for demonstration."""
weather_data = {
"new york": {"temperature": "52°F", "condition": "Partly cloudy", "humidity": "65%"},
"london": {"temperature": "48°F", "condition": "Rainy", "humidity": "80%"},
"tokyo": {"temperature": "68°F", "condition": "Sunny", "humidity": "45%"},
"sydney": {"temperature": "75°F", "condition": "Clear", "humidity": "55%"},
}
city_lower = city.lower()
if city_lower in weather_data:
return json.dumps(weather_data[city_lower])
return json.dumps({"error": f"Weather data not available for {city}"})
# --- Bedrock Client Setup ---
client = boto3.client("bedrock-runtime", region_name="us-east-1")
model_id = "amazon.nova-micro-v1:0"
# --- System Prompt ---
system = [{"text": "You are a helpful weather assistant. You can check the weather for users. Be concise and friendly."}]
# --- Tool Configuration ---
tool_config = {
"tools": [
{
"toolSpec": {
"name": "get_weather",
"description": "Gets the current weather for a given city.",
"inputSchema": {
"json": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The name of the city to get weather for."
}
},
"required": ["city"]
}
}
}
}
]
}
# --- Map tool names to functions ---
tool_functions = {
"get_weather": get_weather
}
# --- Conversation History ---
conversation = []
def chat(user_input):
"""Send a message and handle tool use if requested."""
# Add user message to history
conversation.append({
"role": "user",
"content": [{"text": user_input}]
})
try:
# Call Bedrock Converse API
response = client.converse(
modelId=model_id,
messages=conversation,
system=system,
inferenceConfig={"maxTokens": 512, "temperature": 0.5},
toolConfig=tool_config,
)
# Get the assistant message and stop reason
assistant_message = response["output"]["message"]
stop_reason = response["stopReason"]
# Add assistant response to history
conversation.append(assistant_message)
# Check if the model wants to use a tool
if stop_reason == "tool_use":
# Find the toolUse block
tool_use_block = None
for block in assistant_message["content"]:
if "toolUse" in block:
tool_use_block = block["toolUse"]
break
if tool_use_block:
tool_name = tool_use_block["name"]
tool_input = tool_use_block["input"]
tool_use_id = tool_use_block["toolUseId"]
print(f" [Calling tool: {tool_name}({tool_input})]")
# Run the tool
result = tool_functions[tool_name](**tool_input)
# Send tool result back to the model
conversation.append({
"role": "user",
"content": [
{
"toolResult": {
"toolUseId": tool_use_id,
"content": [{"text": result}]
}
}
]
})
# Get the final response
follow_up = client.converse(
modelId=model_id,
messages=conversation,
system=system,
inferenceConfig={"maxTokens": 512, "temperature": 0.5},
toolConfig=tool_config,
)
final_message = follow_up["output"]["message"]
conversation.append(final_message)
return final_message["content"][0]["text"]
# No tool use - return the text directly
return assistant_message["content"][0]["text"]
except ClientError as e:
return f"ERROR: {e}"
# --- Main Loop ---
def main():
print("Weather Chatbot (powered by Amazon Bedrock)")
print("Type 'quit' to exit.\n")
while True:
user_input = input("You: ").strip()
if user_input.lower() in ("quit", "exit"):
print("Goodbye!")
break
if not user_input:
continue
response = chat(user_input)
print(f"Bot: {response}\n")
if __name__ == "__main__":
main()
Secret mission
Swap Models Without Changing Code
The Converse API is model-agnostic. Your challenge: try a different foundation model by only changing the model_id variable in your script. No other code changes needed.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on-demand through the Amazon Bedrock API. There are no persistent AWS resources (no Lambda functions, S3 buckets, or EC2 instances) running in the background.
Decide whether to keep your files and model access for future experimentation, or clean everything up.
Why no cost warning?
Amazon Bedrock charges only per token used. Amazon Nova Micro costs $0.035 per 1M input tokens and $0.14 per 1M output tokens. For this project, you likely used fewer than 10,000 tokens total, well under $0.01.
If you stop running your script, you stop being charged. There is nothing running in the background that accumulates cost.
Resources you used:
- Local file: chatbot.py on your Desktop.
- Amazon Bedrock model access enabled (Amazon Nova Micro and any additional models).
- Python package: boto3 installed via pip.
Keep everything running
No action needed. Choose this if you want to keep experimenting with your chatbot or try new models.
- Your chatbot.py file stays on your Desktop. You can run it anytime.
- Bedrock model access remains enabled. You are only charged when you actually run the script and make API calls.
- The boto3 package stays installed for future Python projects that use AWS.
Pause - I'll come back to this later
Since Bedrock only charges per API call, there is nothing to pause. Your setup will be ready whenever you return.
- Stop running the chatbot script in your terminal by typing quit or pressing Ctrl+C.
- Your files, packages, and model access will all be waiting for you when you come back.
Delete - I don't want to use this again
Remove all project files and packages to start fresh.
- Delete your project file by running this command (macOS/Linux):
cd ~/Desktop
rm chatbot.py
- On Windows, run this instead:
cd ~\Desktop
del chatbot.py
- Uninstall the boto3 package by running:
pip uninstall boto3 -y
- Bedrock model access does not need to be revoked. You are only charged per token when you make API calls. Leaving model access enabled with no API calls results in zero cost.
- If you want to prevent any future charges entirely, you can restrict Bedrock access by updating the IAM policy attached to your user. Remove the bedrock:InvokeModel permission from your IAM user or group.
That's a Wrap!
That's a Wrap!
Nice work! You just built a terminal-based AI chatbot powered by Amazon Bedrock and Amazon Nova Micro.
You went from a single API call all the way to a tool-equipped conversational assistant that can look up real-world data. That's the same pattern behind every AI-powered application on AWS.
You've learned how to:
- Made your first API call to Amazon Bedrock using the Converse API.
- Built a multi-turn chatbot that maintains conversation history across messages.
- Added tool use so your chatbot can call Python functions and answer real-world questions.
- Compared multiple AI model providers through Bedrock's single, unified interface.
Ready to quiz yourself?