Serverless AI Customer Service Bot
Build an AI customer service API with Lambda, DynamoDB, and Bedrock.
Introduction
30 Second Summary
Every time you message a company's support chat, there's an entire system working behind the scenes to understand your question, remember what you said earlier, and respond without breaking any rules.
In this project, you will build that system from scratch. You'll deploy a serverless AI customer service bot on AWS that responds to messages, remembers conversations, and validates its own replies against business rules.
What You'll Build
A production-style AI customer service API for a fictional company called TechCo, deployed entirely using AWS CLI commands and powered by Amazon Bedrock.
By the end of this project, you'll have:
- A Python Lambda function that orchestrates AI-powered conversations with memory and business rule enforcement.
- A DynamoDB table storing conversation history so the bot remembers what each customer said.
- A live REST API endpoint wired through API Gateway that anyone can send messages to.
- A response validation layer that checks every AI reply against business rules before it reaches the customer.
- Secret Mission: Add SNS-powered escalation alerts that notify a human agent when the bot detects it cannot handle a request.
Are there any prerequisites?
You'll need an AWS account with billing enabled and Python 3 installed on your machine. Step 1 walks you through installing the AWS CLI and configuring everything else from scratch.
Set Up Your Environment
Before you can build your AI customer service bot, you need the tools and permissions in place to talk to AWS from your local machine.
In this step, you will install the AWS CLI, create an IAM role that your Lambda function will use, and enable access to the AI model that powers the bot's responses.
In this step, get ready to:
- Install and configure the AWS CLI on Windows.
- Create an IAM role with permissions for Bedrock, DynamoDB, and CloudWatch.
- Enable Amazon Bedrock model access for Amazon Nova Micro.
Install and configure the AWS CLI
The AWS CLI is a command-line tool that lets you manage AWS services directly from your terminal. You will use it throughout this project to create resources and deploy your bot.
- Press the Windows key to open your search bar.
- Type PowerShell and press Enter to open it.
- Install the AWS CLI by running this command:
msiexec.exe /i https://awscli.amazonaws.com/AWSCLIV2.msi
What does this command do?
This downloads and launches the AWS CLI installer for Windows. A setup wizard will appear. Accept the default settings and click through until it finishes.
- Follow the installer prompts, accepting the default settings at each step.
- Once the installer finishes, close and reopen PowerShell so it picks up the new installation.
- Verify the installation by running:
aws --version
You should see a version string starting with aws-cli/2 followed by version numbers.
Command not found?
If PowerShell does not recognize the aws command, make sure you closed and reopened PowerShell after the installer finished. The installer adds AWS CLI to your system PATH, but existing terminal sessions do not pick it up automatically.
help me troubleshoot why aws --version is not working in PowerShell after installing the AWS CLI
Now configure the CLI with your AWS credentials so it can make API calls on your behalf.
- Run the configure command:
aws configure
- When prompted, enter your AWS Access Key ID and AWS Secret Access Key. You can find these in the AWS Console under IAM > Users > your user > Security credentials > Create access key.
- For Default region name, enter us-east-1.
- For Default output format, enter json.
Why us-east-1?
Amazon Bedrock and its models are available in specific regions. The us-east-1 region (N. Virginia) has the widest model availability and is the default for most AWS tutorials.
- Confirm your credentials work by running:
aws sts get-caller-identity
You should see a JSON response containing your Account, UserId, and Arn.
Seeing an access denied or invalid credentials error?
Double-check that you copied the full Access Key ID and Secret Access Key without extra spaces. You can re-run aws configure to re-enter them.
help me fix AWS CLI credential errors after running aws configure
Create the IAM role for your Lambda function
Your Lambda function needs permission to call Bedrock, read and write to DynamoDB, and send logs to CloudWatch. You will create an IAM role with a trust policy that allows Lambda to assume it, then attach the required permission policies.
- Create the trust policy file by running this command:
@'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
'@ | Out-File -Encoding utf8 trust-policy.json
What is a trust policy?
A trust policy defines which AWS service is allowed to "assume" (use) a role. This policy says lambda.amazonaws.com can assume the role, which means your Lambda function can use the permissions attached to it.
- Create the IAM role by running:
aws iam create-role --role-name customer-service-bot-role --assume-role-policy-document file://trust-policy.json
You should see a JSON response containing the role details including its Arn. Copy the Arn value and save it somewhere handy. You will need it later when deploying your Lambda function.
- Attach the DynamoDB permissions policy by running:
aws iam attach-role-policy --role-name customer-service-bot-role --policy-arn arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess
- Attach the Bedrock permissions policy by running:
aws iam attach-role-policy --role-name customer-service-bot-role --policy-arn arn:aws:iam::aws:policy/AmazonBedrockFullAccess
- Attach the CloudWatch Logs permissions policy by running:
aws iam attach-role-policy --role-name customer-service-bot-role --policy-arn arn:aws:iam::aws:policy/CloudWatchLogsFullAccess
What do these three policies allow?
AmazonDynamoDBFullAccess lets your Lambda function read and write conversation history. AmazonBedrockFullAccess lets it call the AI model. CloudWatchLogsFullAccess lets it write execution logs for monitoring and debugging.
- Verify the role exists and the policies are attached by running:
aws iam get-role --role-name customer-service-bot-role
You should see a JSON response showing the role's RoleName as customer-service-bot-role and the AssumeRolePolicyDocument referencing lambda.amazonaws.com.
Seeing a "NoSuchEntity" or "MalformedPolicyDocument" error?
If create-role fails with a malformed policy error, make sure the trust-policy.json file was saved correctly. You can check its contents by running Get-Content trust-policy.json in PowerShell and comparing it to the JSON above.
If attach-role-policy fails with NoSuchEntity, confirm the role was created first by running aws iam get-role --role-name customer-service-bot-role.
help me troubleshoot IAM role creation errors in the AWS CLI
Enable Amazon Bedrock model access for Nova Micro
Amazon Bedrock requires you to explicitly enable access to each AI model before your code can call it. This is a one-time console step that cannot be done via the CLI.
- Open the AWS Console in your browser and sign in.
- Make sure you are in the us-east-1 region. Check the region selector in the top-right corner of the console.
- In the search bar at the top, type Bedrock and select Amazon Bedrock.
- In the left sidebar, click Model access.
- Click Manage model access (or Modify model access).
- Find Amazon Nova Micro in the list and check the box next to it.
- Click Next, then Submit.
Access is typically granted within a few seconds. You will see the status change to Access granted next to Amazon Nova Micro.
Why can't this be done via the CLI?
Enabling model access is a subscription action handled through the AWS Marketplace integration in the console. Once granted, your account can use the model indefinitely without repeating this step.
Not seeing Amazon Nova Micro in the list?
Make sure your region is set to us-east-1 (N. Virginia) in the top-right corner of the console. Model availability varies by region, and Nova Micro may not appear in all regions.
help me find and enable Amazon Nova Micro model access in the Bedrock console
Your environment is ready. You have the AWS CLI configured, an IAM role with the right permissions, and Bedrock model access enabled. Next up, you will create the DynamoDB table that stores your bot's conversation history.
Create the DynamoDB Table
Your environment is ready and the IAM role is in place. Now it's time to create the database that gives your bot a memory.
Every customer service interaction builds on what came before. Without memory, your bot would ask the same clarifying questions over and over. In this step, you'll create a DynamoDB table designed to store conversation history so the bot can pick up where it left off.
In this step, get ready to:
- Create a DynamoDB table with session-based keys using the AWS CLI.
- Verify the table is active and test read/write access with a sample item.
Create the conversation history table
Your bot needs a table that can store every message in a conversation, ordered by time. The data model uses a composite primary key: a partition key session_id groups all messages in one conversation, and a sort key timestamp keeps them in chronological order.
- Create the DynamoDB table by running this command in your PowerShell terminal:
aws dynamodb create-table `
--table-name CustomerServiceHistory `
--attribute-definitions AttributeName=session_id,AttributeType=S AttributeName=timestamp,AttributeType=S `
--key-schema AttributeName=session_id,KeyType=HASH AttributeName=timestamp,KeyType=RANGE `
--billing-mode PAY_PER_REQUEST
What does this command do?
This single command provisions a fully managed NoSQL table in your AWS account.
- session_id (HASH) is the partition key. All messages from one conversation share the same session ID, so DynamoDB stores them together for fast retrieval.
- timestamp (RANGE) is the sort key. This ensures messages within a session are automatically ordered by time.
- PAY_PER_REQUEST means on-demand billing. You only pay for reads and writes you actually perform, with no capacity planning needed.
- You should see a JSON response with the table description, including "TableStatus": "CREATING".
- Wait about 5-10 seconds for the table to become active.
Why on-demand billing?
On-demand mode removes the need to predict how much read and write capacity you need. For a learning project with unpredictable usage patterns, this keeps costs at zero until you actually start sending messages.
The DynamoDB free tier covers 25 read and 25 write request units, which is more than enough for testing.
Verify the table and test read/write access
Before moving on, you need to confirm the table is live and that your IAM role has the correct permissions to read and write data.
- Check that your table is active by running:
aws dynamodb describe-table --table-name CustomerServiceHistory
- Look for "TableStatus": "ACTIVE" in the output. If you still see CREATING, wait a few more seconds and run the command again.
Seeing ResourceNotFoundException?
DynamoDB uses eventually consistent reads for table metadata. If you run describe-table immediately after create-table, you might get this error. Wait 5-10 seconds and try again.
If the error persists, double-check that the table name CustomerServiceHistory matches exactly (it's case-sensitive).
Why is my DynamoDB table not showing as active?
- Insert a test item to confirm write access by running:
aws dynamodb put-item `
--table-name CustomerServiceHistory `
--item '{"session_id": {"S": "test-session-001"}, "timestamp": {"S": "2024-01-01T00:00:00Z"}, "role": {"S": "user"}, "message": {"S": "Hello, I need help with my order."}}'
What does this test item represent?
This item simulates a customer's first message in a support session. The four fields match the exact schema your Lambda function will use later:
- session_id groups messages into one conversation.
- timestamp records when the message was sent.
- role indicates whether it's from the user or the assistant.
- message stores the actual text.
- If the command succeeds, you won't see any output. That's normal for put-item.
- Now query the item back to confirm read access by running:
aws dynamodb query `
--table-name CustomerServiceHistory `
--key-condition-expression "session_id = :sid" `
--expression-attribute-values '{":sid": {"S": "test-session-001"}}'
- You should see a JSON response containing your test item with "Count": 1 and the full item in the Items array.
What does this query do?
The --key-condition-expression flag tells DynamoDB to find all items where session_id equals the value you provide. The --expression-attribute-values flag substitutes the placeholder :sid with the actual session ID value at runtime. This is the same pattern your Lambda function will use to fetch conversation history.
Query returned zero items?
Make sure the session ID in your query matches exactly what you used in the put-item command: test-session-001. String comparisons in DynamoDB are case-sensitive.
Also verify the put-item command did not return an error. If you saw an AccessDeniedException, your configured CLI user may not have DynamoDB permissions. Check that your IAM user or role has the AmazonDynamoDBFullAccess policy attached.
Help me debug my DynamoDB query returning empty results.
Your database is live and confirmed working. Next up, you'll write the Lambda function that connects to this table and uses it to give your bot a memory.
Write the Lambda Function
Your infrastructure is ready. The DynamoDB table is waiting for conversation data, and the IAM role has the permissions your function needs.
Now it's time to write the Python code that ties everything together. Your Lambda function will receive a customer message, look up that customer's conversation history, ask Amazon Bedrock to generate a response, validate that response against business rules, and save the exchange for future context.
In this step, get ready to:
- Create the Lambda function file with client initialization and helper functions for conversation history.
- Add response validation logic that enforces business rules.
- Add the save function and main handler that orchestrates the full conversation flow.
Set up the Lambda function file
The Lambda function needs to connect to multiple AWS services. You will initialize clients for Bedrock (to generate AI responses) and DynamoDB (to store conversations), then define the business rules that control how the bot behaves.
- Create a new file called lambda_function.py in your project folder by running this command:
New-Item -Path .\lambda_function.py -ItemType File
- Open lambda_function.py in your text editor and paste the following code:
import json
import boto3
import os
from datetime import datetime, timezone
from boto3.dynamodb.conditions import Key
# Initialize clients
bedrock = boto3.client("bedrock-runtime", region_name=os.environ["AWS_REGION"])
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(os.environ["TABLE_NAME"])
# Business rules for the AI assistant
SYSTEM_PROMPT = """You are a helpful customer service assistant for TechCo, an electronics company.
Business Rules:
- Always be polite and professional
- Never discuss competitor products
- Do not provide refunds over $100 without escalating
- If a customer is angry or the issue is complex, suggest speaking with a human agent
- Keep responses concise (under 150 words)
- Never share internal company information or policies marked as confidential
"""
PROHIBITED_PHRASES = [
"confidential",
"internal only",
"competitor",
]
What does this code do?
The imports bring in the AWS SDK (boto3) and utilities for JSON handling, timestamps, and DynamoDB queries.
The three clients connect to different services. bedrock talks to the AI model. dynamodb and table connect to your CustomerServiceHistory table. Both read their configuration from environment variables that you will set during deployment.
The SYSTEM_PROMPT tells the AI how to behave. It defines the company identity (TechCo), sets tone rules, and establishes escalation boundaries. PROHIBITED_PHRASES is a list of words the validation layer will block from responses.
- Save lambda_function.py.
- Confirm the file exists by running:
ls lambda_function.py
- You should see lambda_function.py listed in the output.
Don't see the file?
- Make sure you ran the New-Item command from the same folder where you are now. Run Get-Location to check your current directory.
- If you created the file in a different folder, navigate to that folder or recreate it here.
Still stuck? Help me find or create lambda_function.py in the right folder.
Now add the two helper functions that retrieve and format conversation history. These functions prepare the data that the AI model needs to understand the conversation context.
- In lambda_function.py, add the following code below the PROHIBITED_PHRASES list:
def get_conversation_history(session_id):
"""Retrieve conversation history from DynamoDB."""
response = table.query(
KeyConditionExpression=Key("session_id").eq(session_id),
ScanIndexForward=True,
Limit=10
)
return response.get("Items", [])
def build_messages(history, new_message):
"""Build the messages array for Bedrock from conversation history."""
messages = []
for item in history:
messages.append({
"role": item["role"],
"content": [{"text": item["message"]}]
})
messages.append({
"role": "user",
"content": [{"text": new_message}]
})
return messages
What do these functions do?
- get_conversation_history queries DynamoDB for the last 10 messages in a session, sorted by timestamp. This gives the AI model context about what the customer already said.
- build_messages converts the stored history into the format that the Bedrock Converse API expects. Each message becomes an object with a role (user or assistant) and content array. The new user message is appended at the end.
- Save lambda_function.py.
You won't see a change yet. The main handler (coming in a later substep) will call both of these functions to build the AI's conversation context.
Add the response validation function
Before any AI-generated response reaches the customer, it needs to pass a safety check. The validation function scans responses for prohibited content and enforces a maximum length.
- In lambda_function.py, add this function below build_messages:
def validate_response(response_text):
"""Validate the AI response against business rules."""
lower_response = response_text.lower()
for phrase in PROHIBITED_PHRASES:
if phrase in lower_response:
return False, f"Response contained prohibited content: {phrase}"
if len(response_text) > 1000:
return False, "Response exceeded maximum length"
return True, "OK"
What does this function do?
validate_response checks every AI response before it reaches the customer. It loops through PROHIBITED_PHRASES and rejects any response containing those words. It also rejects responses over 1000 characters.
The function returns a tuple: a boolean (pass/fail) and a message explaining why. If validation fails, the handler will substitute a safe fallback message instead of sending the AI's original response.
- Save lambda_function.py.
You won't see a change yet. The handler will call this function after every Bedrock response to decide whether the reply is safe to send.
Why validate AI responses?
AI models can occasionally produce outputs that violate business rules. They might accidentally mention a competitor, reveal something marked confidential, or generate an excessively long reply.
A validation layer acts as a safety net. In production systems, this pattern is standard. It ensures the bot never sends something that could damage the company's reputation or violate compliance rules.
Add the save function and complete the handler
The final piece is the save_message function and the main lambda_handler that orchestrates the entire conversation flow. The handler parses the incoming request, retrieves history, calls Bedrock, validates the response, saves messages, and returns the result.
- In lambda_function.py, add this function below validate_response:
def save_message(session_id, role, message):
"""Save a message to the conversation history."""
table.put_item(
Item={
"session_id": session_id,
"timestamp": datetime.now(timezone.utc).isoformat(),
"role": role,
"message": message
}
)
What does this function do?
save_message writes a single message to the CustomerServiceHistory DynamoDB table. It stores the session ID, a UTC timestamp, the role (user or assistant), and the message text.
The handler calls this twice per request. Once for the customer's message, and once for the bot's response. This builds up the conversation history that get_conversation_history retrieves on the next request.
Now add the main handler that ties everything together. This is the function that AWS Lambda invokes when a request arrives.
- Below save_message, add the main handler function:
def lambda_handler(event, context):
"""Main Lambda handler for the customer service bot."""
try:
body = json.loads(event.get("body", "{}"))
session_id = body.get("session_id")
user_message = body.get("message")
if not session_id or not user_message:
return {
"statusCode": 400,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"error": "session_id and message are required"})
}
# Get conversation history
history = get_conversation_history(session_id)
# Build messages for Bedrock
messages = build_messages(history, user_message)
# Call Amazon Bedrock
model_response = bedrock.converse(
modelId="amazon.nova-micro-v1:0",
messages=messages,
system=[{"text": SYSTEM_PROMPT}],
inferenceConfig={"maxTokens": 300, "temperature": 0.3, "topP": 0.9}
)
assistant_message = model_response["output"]["message"]["content"][0]["text"]
# Validate response
is_valid, validation_message = validate_response(assistant_message)
if not is_valid:
assistant_message = "I apologize, but I need to connect you with a human agent for further assistance. Please hold while I transfer you."
# Save messages to history
save_message(session_id, "user", user_message)
save_message(session_id, "assistant", assistant_message)
return {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({
"session_id": session_id,
"response": assistant_message,
"validated": is_valid
})
}
except Exception as e:
print(f"Error: {str(e)}")
return {
"statusCode": 500,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"error": "Internal server error"})
}
What does this handler do?
- Parses the incoming API Gateway event to extract session_id and message from the request body.
- Returns a 400 error if either field is missing.
- Calls get_conversation_history and build_messages to prepare context for the AI.
- Sends the conversation to Amazon Bedrock using the Converse API with the Nova Micro model.
- Validates the AI's response. If it fails, substitutes a safe fallback that escalates to a human agent.
- Saves both the user message and the assistant response to DynamoDB for future context.
- Returns the response with a 200 status code. The validated field tells the caller whether the original response passed business rules.
- Save lambda_function.py.
- Verify your file has no syntax errors by running:
python -c "import lambda_function"
What should I see?
If the file is syntactically valid, the command produces no output and returns to the prompt. That silence means success.
If you see a ModuleNotFoundError for boto3, that is expected if you do not have boto3 installed locally. The Lambda runtime includes boto3 automatically, so this error does not affect deployment. A SyntaxError means you have a typo that needs fixing.
Seeing a SyntaxError?
- Check that all triple-quoted strings (""") are properly closed. The SYSTEM_PROMPT string must have opening and closing triple quotes.
- Make sure every function definition has a colon at the end of the def line.
- Verify that the f-string in the validate_response function uses curly braces {phrase} without extra quotes around them.
Need help? Help me fix a SyntaxError in my lambda_function.py file.
✔️ Awesome, I've got everything!
Great. Double check you have saved lambda_function.py and the syntax check passed (or only showed a ModuleNotFoundError for boto3).
ⓧ I'd like to double check the full code
Here is the complete lambda_function.py file. Compare it against yours to find any differences:
import json
import boto3
import os
from datetime import datetime, timezone
from boto3.dynamodb.conditions import Key
# Initialize clients
bedrock = boto3.client("bedrock-runtime", region_name=os.environ["AWS_REGION"])
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(os.environ["TABLE_NAME"])
# Business rules for the AI assistant
SYSTEM_PROMPT = """You are a helpful customer service assistant for TechCo, an electronics company.
Business Rules:
- Always be polite and professional
- Never discuss competitor products
- Do not provide refunds over $100 without escalating
- If a customer is angry or the issue is complex, suggest speaking with a human agent
- Keep responses concise (under 150 words)
- Never share internal company information or policies marked as confidential
"""
PROHIBITED_PHRASES = [
"confidential",
"internal only",
"competitor",
]
def get_conversation_history(session_id):
"""Retrieve conversation history from DynamoDB."""
response = table.query(
KeyConditionExpression=Key("session_id").eq(session_id),
ScanIndexForward=True,
Limit=10
)
return response.get("Items", [])
def build_messages(history, new_message):
"""Build the messages array for Bedrock from conversation history."""
messages = []
for item in history:
messages.append({
"role": item["role"],
"content": [{"text": item["message"]}]
})
messages.append({
"role": "user",
"content": [{"text": new_message}]
})
return messages
def validate_response(response_text):
"""Validate the AI response against business rules."""
lower_response = response_text.lower()
for phrase in PROHIBITED_PHRASES:
if phrase in lower_response:
return False, f"Response contained prohibited content: {phrase}"
if len(response_text) > 1000:
return False, "Response exceeded maximum length"
return True, "OK"
def save_message(session_id, role, message):
"""Save a message to the conversation history."""
table.put_item(
Item={
"session_id": session_id,
"timestamp": datetime.now(timezone.utc).isoformat(),
"role": role,
"message": message
}
)
def lambda_handler(event, context):
"""Main Lambda handler for the customer service bot."""
try:
body = json.loads(event.get("body", "{}"))
session_id = body.get("session_id")
user_message = body.get("message")
if not session_id or not user_message:
return {
"statusCode": 400,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"error": "session_id and message are required"})
}
# Get conversation history
history = get_conversation_history(session_id)
# Build messages for Bedrock
messages = build_messages(history, user_message)
# Call Amazon Bedrock
model_response = bedrock.converse(
modelId="amazon.nova-micro-v1:0",
messages=messages,
system=[{"text": SYSTEM_PROMPT}],
inferenceConfig={"maxTokens": 300, "temperature": 0.3, "topP": 0.9}
)
assistant_message = model_response["output"]["message"]["content"][0]["text"]
# Validate response
is_valid, validation_message = validate_response(assistant_message)
if not is_valid:
assistant_message = "I apologize, but I need to connect you with a human agent for further assistance. Please hold while I transfer you."
# Save messages to history
save_message(session_id, "user", user_message)
save_message(session_id, "assistant", assistant_message)
return {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({
"session_id": session_id,
"response": assistant_message,
"validated": is_valid
})
}
except Exception as e:
print(f"Error: {str(e)}")
return {
"statusCode": 500,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"error": "Internal server error"})
}
Your Lambda function code is complete. Next up, you will package it and deploy it to AWS along with the API Gateway endpoint that makes it accessible over the internet.
Deploy Lambda and Wire Up API Gateway
Your Lambda function code is written and validated. Now it needs a home in the cloud where it can actually run.
In this step, you will package your function, deploy it to AWS Lambda, then create an API Gateway REST endpoint so anyone on the internet can send messages to your bot.
In this step, get ready to:
- Package and deploy your Lambda function to AWS.
- Create an API Gateway REST API with a POST endpoint.
- Deploy the API and get your public invoke URL.
Package and deploy the Lambda function
AWS Lambda expects your code as a ZIP archive. Since boto3 is pre-installed in the Lambda Python runtime, you only need to package your single lambda_function.py file.
- Create the deployment ZIP by running this command in PowerShell:
Compress-Archive -Path lambda_function.py -DestinationPath function.zip
What does this command do?
PowerShell's Compress-Archive takes your lambda_function.py file and creates a function.zip archive in the same directory. This is the format Lambda expects for deployment.
- Confirm the ZIP was created by running:
ls function.zip
You should see function.zip listed in the output.
- Deploy the Lambda function to AWS by running this command:
aws lambda create-function `
--function-name customer-service-bot `
--runtime python3.13 `
--handler lambda_function.lambda_handler `
--role [[ROLE_ARN="your IAM role ARN from Step 1"]] `
--zip-file fileb://function.zip `
--environment "Variables={TABLE_NAME=CustomerServiceHistory,AWS_REGION=us-east-1}" `
--timeout 30
What does this command do?
- --function-name names your Lambda function customer-service-bot.
- --runtime python3.13 tells Lambda which Python version to use.
- --handler lambda_function.lambda_handler points Lambda to the lambda_handler function inside lambda_function.py.
- --role assigns the IAM role you created in Step 1 so the function can access Bedrock, DynamoDB, and CloudWatch.
- --environment sets the TABLE_NAME and AWS_REGION variables your code reads at runtime.
- --timeout 30 gives the function 30 seconds to respond, which is enough time for Bedrock to generate an answer.
You should see a JSON response that includes your function's ARN and state as Active. Copy the FunctionArn value from the output. You will need it in the next substep.
Seeing an InvalidParameterValueException?
- Make sure the role ARN you pasted starts with arn:aws:iam:: and includes the full path to customer-service-bot-role.
- If you see a message about the role not being assumable, wait 10 seconds and try again. New IAM roles sometimes take a moment to propagate.
- help me fix my Lambda create-function error
Create the API Gateway REST API and wire it to Lambda
Your Lambda function is live, but there is no way for the outside world to reach it yet. You will create a REST API with a /chat endpoint that forwards incoming requests directly to your Lambda.
- Create the REST API by running:
aws apigateway create-rest-api --name CustomerServiceBot
Copy the id value from the output. This is your API ID that you will use in every following command.
- Get the root resource ID by running:
aws apigateway get-resources --rest-api-id [[API_ID="your API ID from the previous command"]]
The output shows a resource with path set to /. Copy its id value. This is the root resource ID.
- Create the /chat resource by running:
aws apigateway create-resource `
--rest-api-id [[API_ID="your API ID"]] `
--parent-id [[ROOT_RESOURCE_ID="your root resource ID"]] `
--path-part chat
Copy the id from this output. This is your chat resource ID.
- Add a POST method to the /chat resource by running:
aws apigateway put-method `
--rest-api-id [[API_ID="your API ID"]] `
--resource-id [[CHAT_RESOURCE_ID="your chat resource ID"]] `
--http-method POST `
--authorization-type NONE
- Connect the POST method to your Lambda function using AWS_PROXY integration by running:
aws apigateway put-integration `
--rest-api-id [[API_ID="your API ID"]] `
--resource-id [[CHAT_RESOURCE_ID="your chat resource ID"]] `
--http-method POST `
--type AWS_PROXY `
--integration-http-method POST `
--uri "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/[[FUNCTION_ARN="your Lambda function ARN"]]/invocations"
What is AWS_PROXY integration?
AWS_PROXY (also called Lambda proxy integration) passes the entire HTTP request straight to your Lambda function. Your function receives the headers, body, and query parameters as-is, and returns a response with statusCode, headers, and body.
The --integration-http-method POST is required for all Lambda integrations regardless of the client-facing HTTP method. Lambda always receives invocations via POST.
- Grant API Gateway permission to invoke your Lambda function by running:
aws lambda add-permission `
--function-name customer-service-bot `
--statement-id apigateway-invoke `
--action lambda:InvokeFunction `
--principal apigateway.amazonaws.com `
--source-arn "arn:aws:execute-api:us-east-1:[[AWS_ACCOUNT_ID="your 12-digit AWS account ID"]]:[[API_ID="your API ID"]]/*/POST/chat"
Why is this permission needed?
AWS services cannot call each other by default. This command creates a resource-based policy on your Lambda that says "API Gateway is allowed to invoke me, but only for POST requests to /chat on this specific API."
Getting an access denied error?
- Double-check that your --source-arn uses the correct API ID and account ID. You can find your account ID by running aws sts get-caller-identity.
- Make sure the function name matches exactly: customer-service-bot.
- help me fix my Lambda add-permission error
Deploy the API and get your invoke URL
Everything is wired up, but the API is not live yet. You need to deploy it to a stage to make it callable from the internet.
- Deploy the API to a prod stage by running:
aws apigateway create-deployment `
--rest-api-id [[API_ID="your API ID"]] `
--stage-name prod
What does deploying to a stage mean?
A stage is a named snapshot of your API configuration. Deploying to prod makes your API available at a public URL. You could also create dev or staging stages for testing without affecting production.
Your invoke URL follows this pattern:
https://<api-id>.execute-api.<region>.amazonaws.com/prod/chat
- Build your invoke URL by replacing <api-id> with your API ID and <region> with us-east-1 (or whatever region you configured).
- Save this URL somewhere. You will use it to test your bot in the next step.
- Verify the deployment worked by sending a quick test request:
Invoke-WebRequest -Uri "https://[[API_ID="your API ID"]].execute-api.us-east-1.amazonaws.com/prod/chat" `
-Method POST `
-ContentType "application/json" `
-Body '{"session_id": "test-123", "message": "Hello"}'
You should see a response with a StatusCode of 200 and a JSON body containing the bot's reply. If you see a 200 status, your entire stack is working.
Getting a 500 Internal Server Error?
- This usually means your Lambda function is crashing. Check CloudWatch Logs by running aws logs describe-log-groups to find the log group, then check for error messages.
- Verify your environment variables are correct. The TABLE_NAME must be exactly CustomerServiceHistory and AWS_REGION must match your deployment region.
- Make sure you enabled Bedrock model access for Amazon Nova Micro in Step 1. Without it, the Bedrock call fails silently.
- help me debug my 500 error from the customer service bot API
Your serverless backend is fully deployed. You have a Lambda function processing requests, API Gateway routing traffic from the public internet, and DynamoDB storing conversations. Next up, you will put the bot through its paces by testing conversation memory and business rules.
Test and Monitor Your Bot
Your entire serverless stack is deployed and connected. The API Gateway endpoint is live, your Lambda function is ready to execute, and DynamoDB is waiting to store conversations.
Now you need to prove the system actually works end-to-end. You will send real messages to the API, verify the bot responds with context-aware answers, confirm that conversation memory persists across multiple messages, and check CloudWatch Logs to make sure nothing is failing silently.
In this step, get ready to:
- Send test messages to your API and verify the bot follows business rules.
- Confirm conversation memory by sending follow-up messages in the same session.
- Review CloudWatch Logs to verify successful Lambda invocations.
Send your first test message
To talk to your bot, you need to send a POST request to the API Gateway invoke URL you noted in the previous step. The request body must include a session_id and a message field.
- Store your API URL in a PowerShell variable by running this command:
$apiUrl = "[[API_URL="https://your-api-id.execute-api.us-east-1.amazonaws.com/prod/chat"]]"
Where do I find my invoke URL?
You noted this URL at the end of Step 4 when you deployed the API to the prod stage. It follows the format https://<api-id>.execute-api.<region>.amazonaws.com/prod/chat.
- Send your first customer service message by running this command:
$response = Invoke-WebRequest -Uri $apiUrl -Method POST -Body '{"session_id": "test-session-1", "message": "Hi, I bought a laptop last week and the screen is flickering. Can you help?"}' -ContentType "application/json"
$response.Content
What does this command do?
Invoke-WebRequest sends an HTTP request from PowerShell. The -Method POST flag tells it to send data rather than just request a page.
The -Body parameter contains JSON with a session ID (like a ticket number) and the customer's message. -ContentType "application/json" tells the server to expect JSON.
- Check the output in your terminal.
You should see a JSON response containing a session_id, a response field with the bot's reply, and a validated field set to true. The bot should offer helpful troubleshooting advice about the flickering screen in a polite, professional tone.
Seeing an Internal server error?
If the response shows Internal server error, the Lambda function encountered a problem. Common causes:
- The IAM role is missing Bedrock permissions. Verify by running aws iam list-attached-role-policies --role-name customer-service-bot-role.
- Bedrock model access was not enabled for Amazon Nova Micro. Return to the Bedrock console and confirm amazon.nova-micro-v1:0 shows as granted.
- The environment variable TABLE_NAME or AWS_REGION is not set on the Lambda function.
Help me debug my customer service bot's Internal server error
Test conversation memory with a follow-up message
A real customer service bot needs to remember what was said earlier in the conversation. Your Lambda stores each message in DynamoDB and retrieves the history on the next call. To prove this works, you will send a second message using the same session_id.
- Send a follow-up message in the same session by running this command:
$response2 = Invoke-WebRequest -Uri $apiUrl -Method POST -Body '{"session_id": "test-session-1", "message": "It only happens when I open video calls. Is this a hardware issue?"}' -ContentType "application/json"
$response2.Content
What should I see?
The bot's response should reference the flickering screen from your first message. It has context because Lambda queried DynamoDB for all prior messages in test-session-1 and passed them to Bedrock as conversation history.
If the bot responds as though it has never heard about the laptop issue, conversation memory is not working. Check that the save_message function in your Lambda is writing to DynamoDB correctly.
- Optionally, test a brand new session to confirm sessions are isolated by running this command:
$response3 = Invoke-WebRequest -Uri $apiUrl -Method POST -Body '{"session_id": "test-session-2", "message": "What is your return policy?"}' -ContentType "application/json"
$response3.Content
This response should have no awareness of the laptop issue from test-session-1. Each session ID creates an independent conversation thread.
Check CloudWatch Logs for successful invocations
Every time your Lambda function runs, it writes execution logs to CloudWatch. These logs help you confirm the function is healthy and catch errors before your users do.
- Retrieve recent log events for your Lambda function by running this command:
aws logs filter-log-events --log-group-name /aws/lambda/customer-service-bot --limit 20
What does this command do?
aws logs filter-log-events searches a CloudWatch log group for events. The --log-group-name flag points to the log group that Lambda automatically creates when your function runs. The --limit 20 flag caps the results to the most recent 20 events so the output stays readable.
- Scan the output for lines containing START, END, and REPORT.
These entries confirm your Lambda executed successfully. The REPORT line shows the duration, memory used, and billed duration for each invocation.
- Check that no lines contain Error or Traceback messages.
Seeing errors in the logs?
If you see AccessDeniedException errors, the IAM role is missing a permission. Check which service is being denied (Bedrock, DynamoDB, or Logs) and verify the corresponding policy is attached.
If the log group does not exist yet, it means your Lambda has never been invoked. Go back and re-send a test message, then try this command again.
Help me understand errors in my Lambda CloudWatch Logs
Secret mission
Add SNS Escalation Alerts
Your bot handles most customer queries. But what happens when a customer is furious, threatens a lawsuit, or demands to speak with a manager? In production, these situations need a human in the loop. In this secret mission, you'll add automatic escalation alerts using Amazon SNS so that a real person gets notified the moment the bot detects it's out of its depth.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. Amazon Bedrock charges per token with no free tier, but only when you actively send messages. All other services stay within the AWS Free Tier for light usage.
Cost warning
Bedrock charges apply every time your API endpoint is called. If you share your invoke URL or leave it publicly accessible, unexpected calls could generate costs. If you are finished testing, consider deleting all resources to ensure zero ongoing charges.
Resources you used:
- DynamoDB table CustomerServiceHistory (on-demand billing, free tier covers 25 read/write request units).
- Lambda function customer-service-bot (free tier covers 1M requests/month).
- API Gateway REST API CustomerServiceBot (free tier covers 1M API calls/month for 12 months).
- IAM role customer-service-bot-role (no cost).
- CloudWatch Logs log group /aws/lambda/customer-service-bot (free tier covers 5 GB).
- SNS topic CustomerServiceEscalation (free tier covers 1,000 email notifications/month).
- Local files lambda_function.py and function.zip on your computer.
Keep everything running
No action needed. Choose this if you are still actively building or testing your bot.
- Bedrock charges only when you send messages to the API endpoint.
- DynamoDB, Lambda, API Gateway, CloudWatch, and SNS all remain within the AWS Free Tier for light usage.
- Your invoke URL stays active. Avoid sharing it publicly to prevent unexpected Bedrock costs.
Pause - I'll come back to this later
Shut down the public endpoint so no one can trigger Bedrock calls while you are away. You can redeploy the stage later to resume.
- Delete the API Gateway prod stage deployment. This removes the public URL but keeps the API configuration intact.
- All other resources remain and incur no charges when idle.
- When you are ready to resume, create a new deployment to the prod stage and your endpoint will be live again.
Delete - I don't want to use this again
Remove all project resources and start fresh. Run these commands in order in your terminal.
Delete the API Gateway REST API:
aws apigateway delete-rest-api --rest-api-id [[API_ID="your REST API ID from the create step"]]
Delete the Lambda function:
aws lambda delete-function --function-name customer-service-bot
Delete the DynamoDB table:
aws dynamodb delete-table --table-name CustomerServiceHistory
Delete the SNS topic (this also removes all subscriptions):
aws sns delete-topic --topic-arn [[SNS_TOPIC_ARN="your SNS topic ARN"]]
Delete the CloudWatch Logs log group:
aws logs delete-log-group --log-group-name /aws/lambda/customer-service-bot
Clean up the IAM role. You must detach all policies before deleting the role. First, list the attached policies:
aws iam list-attached-role-policies --role-name customer-service-bot-role
For each policy ARN returned, detach it:
aws iam detach-role-policy --role-name customer-service-bot-role --policy-arn [[POLICY_ARN="the policy ARN from the list above"]]
If there are inline policies, list and delete them:
aws iam list-role-policies --role-name customer-service-bot-role
aws iam delete-role-policy --role-name customer-service-bot-role --policy-name [[INLINE_POLICY_NAME="the inline policy name from the list above"]]
Once all policies are removed, delete the role:
aws iam delete-role --role-name customer-service-bot-role
Finally, delete the local project files from your computer:
Remove-Item lambda_function.py, function.zip
How do I know everything is deleted?
Run aws lambda list-functions and confirm customer-service-bot no longer appears. Run aws dynamodb list-tables and confirm CustomerServiceHistory is gone. If both are absent, your cleanup is complete.
Nice Work!
Nice Work!
Nice work! You just built and deployed a production-style AI customer service bot entirely on AWS serverless infrastructure.
You've learned how to:
- Build a Python Lambda function that orchestrates AI-powered conversations using Amazon Bedrock, DynamoDB conversation history, and response validation.
- Deploy an entire serverless stack via AWS CLI commands, applying infrastructure-as-code principles without a single console click for resource creation.
- Implement a response validation layer that enforces business rules before AI-generated replies reach the customer, protecting against prohibited content and runaway responses.
- (Secret Mission) Added SNS escalation alerts that notify a human agent when the bot detects it cannot handle a request.
Ready to quiz yourself?