Building your own text summarization app using Amazon Bedrock, and pairing it with Bedrock Guardrails so it doesn’t go rogue!
Architecture
AWS Resources:
- S3: Hosts the frontend of the app (html) API Gateway: Provides REST API endpoint to receive content and return summary
- AWS Lambda: Processes content and invokes Amazon Bedrock with chosen model and route the request through Bedrock Guardrails the return the output
- Amazon Bedrock: Provides access to AI foundation models. For this experiment we are using Claude 3 Haiku for its speed and cost-effectiveness
- Amazon Bedrock Guardrails: Implements safeguards customized to your application requirements and responsible AI policies. IAM: Manages permissions for lambda and Amazon Bedrock access
Prerequisites:
- AWS Account: you will need it to deploy S3, lambda, API Gateway, Bedrock, and Guardrails
- Environment setup: Make sure these are set installed and working
- Note.js
- Typescript
- AWS CDK Toolkit
- Docker: up and running, we will use this to bundle our lambda function
- AWS Credentials: keep them handy so you can deploy the stacks
Deploy
1. Get the model ID
Good news! You do not need to manually grant access to serverless foundation models anymore. They are now automatically enabled for your AWS account.
To get the model ID:
- Go to Amazon Bedrock console
- Find the model you want.
For this one, we are using Claude 3 Haiku because it's fast and cost-effective. The Model ID looks like this: anthropic.claude-3-haiku-20240307-v1:0. You will need this later when updating your IAM policy, so your lambda function can invoke the model.
Always check the pricing for the model you chose so you don't get any surprise charges
2. Create the resource
2.1 Set up the frontend for our summarizer app.
We will create two html files:
- index.html: main interface when the users can paste text and get output
- error.html: a simple page if something goes wrong index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Content Summarizer - AI-Powered Text Summary Tool</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
width: 100%;
max-width: 800px;
height: 90vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
padding: 25px;
text-align: center;
}
.header h1 {
font-size: 28px;
margin-bottom: 10px;
}
.header p {
opacity: 0.9;
font-size: 14px;
}
.stats {
display: flex;
gap: 15px;
justify-content: center;
margin-top: 15px;
flex-wrap: wrap;
}
.stat-item {
background: rgba(255, 255, 255, 0.2);
padding: 8px 15px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
}
.chat-container {
flex: 1;
overflow-y: auto;
padding: 20px;
background: #f8f9fa;
}
.message {
margin-bottom: 15px;
display: flex;
align-items: flex-start;
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message.user {
justify-content: flex-end;
}
.message-content {
max-width: 70%;
padding: 12px 16px;
border-radius: 18px;
word-wrap: break-word;
white-space: pre-wrap;
}
.message.user .message-content {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
border-bottom-right-radius: 4px;
}
.message.bot .message-content {
background: white;
color: #333;
border: 1px solid #e0e0e0;
border-bottom-left-radius: 4px;
}
.message-subject {
font-size: 11px;
opacity: 0.7;
margin-top: 5px;
font-style: italic;
}
.input-container {
padding: 20px;
background: white;
border-top: 1px solid #e0e0e0;
}
.input-wrapper {
display: flex;
gap: 10px;
flex-direction: column;
}
#contentInput {
width: 100%;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 12px;
font-size: 14px;
outline: none;
transition: border-color 0.3s;
resize: vertical;
min-height: 100px;
font-family: inherit;
margin-bottom: 10px;
}
#contentInput:focus {
border-color: #10b981;
}
#summarizeButton {
width: 100%;
padding: 12px 30px;
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
border: none;
border-radius: 25px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
#summarizeButton:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(16, 185, 129, 0.4);
}
#summarizeButton:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.error {
background: #fee;
color: #c33;
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 15px;
}
.info-box {
background: #e3f2fd;
border-left: 4px solid #2196f3;
padding: 15px;
margin-bottom: 20px;
border-radius: 4px;
}
.info-box strong {
color: #1976d2;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📝 Content Summarizer</h1>
<p>AI-powered text summarization tool</p>
<div class="stats">
<span class="stat-item">✨ Instant Summaries</span>
<span class="stat-item">🎯 Key Points</span>
<span class="stat-item">⚡ Fast & Accurate</span>
</div>
</div>
<div class="chat-container" id="chatContainer">
<div class="info-box">
<strong>Welcome!</strong> Paste any text content below and I'll
create a clear, concise summary for you. Perfect for articles,
documents, reports, and more.
</div>
</div>
<div class="input-container">
<textarea
id="contentInput"
placeholder="Paste your content here to summarize..."
></textarea>
<button id="summarizeButton" onclick="summarizeContent()">
Summarize
</button>
</div>
</div>
<script>
// TODO: Replace with your actual API Gateway endpoint after deployment
const API_ENDPOINT = 'YOUR_API_ENDPOINT_HERE';
const chatContainer = document.getElementById('chatContainer');
const contentInput = document.getElementById('contentInput');
const summarizeButton = document.getElementById('summarizeButton');
// Allow Ctrl+Enter to summarize
contentInput.addEventListener('keydown', (e) => {
if (
(e.ctrlKey || e.metaKey) &&
e.key === 'Enter' &&
!summarizeButton.disabled
) {
summarizeContent();
}
});
function addMessage(text, isUser, metadata = null) {
const messageDiv = document.createElement('div');
messageDiv.className = `message ${isUser ? 'user' : 'bot'}`;
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
contentDiv.textContent = text;
if (metadata && !isUser) {
const metaDiv = document.createElement('div');
metaDiv.className = 'message-subject';
metaDiv.textContent = `${metadata.contentType} • ${metadata.wordCount} words`;
contentDiv.appendChild(metaDiv);
}
messageDiv.appendChild(contentDiv);
chatContainer.appendChild(messageDiv);
chatContainer.scrollTop = chatContainer.scrollHeight;
}
function showError(message) {
const errorDiv = document.createElement('div');
errorDiv.className = 'error';
errorDiv.textContent = `Error: ${message}`;
chatContainer.appendChild(errorDiv);
chatContainer.scrollTop = chatContainer.scrollHeight;
}
async function summarizeContent() {
const content = contentInput.value.trim();
if (!content) {
return;
}
if (API_ENDPOINT === 'YOUR_API_ENDPOINT_HERE') {
showError(
'Please update the API_ENDPOINT in the JavaScript code with your deployed API Gateway URL'
);
return;
}
// Add user message (show full content)
addMessage(
`Content to summarize (${content.split(/\s+/).length} words):\n\n${content}`,
true
);
contentInput.value = '';
// Disable input while processing
summarizeButton.disabled = true;
summarizeButton.innerHTML = '<span class="loading"></span>';
contentInput.disabled = true;
try {
const response = await fetch(API_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ content }),
});
const data = await response.json();
if (!response.ok) {
throw new Error(
data.error || data.details || 'Failed to get summary'
);
}
// Add bot response
addMessage(data.summary, false, {
contentType: data.contentType,
wordCount: data.wordCount,
});
} catch (error) {
console.error('Error:', error);
showError(
error.message ||
'Failed to get summary. Please check your API endpoint and ensure Bedrock is enabled.'
);
} finally {
// Re-enable input
summarizeButton.disabled = false;
summarizeButton.textContent = 'Summarize';
contentInput.disabled = false;
contentInput.focus();
}
}
</script>
</body>
</html>
error.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Error - Page Not Found</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.error-container {
background: white;
border-radius: 20px;
padding: 60px 40px;
text-align: center;
max-width: 600px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.error-code {
font-size: 120px;
font-weight: bold;
color: #10b981;
line-height: 1;
margin-bottom: 20px;
}
h1 {
font-size: 32px;
color: #333;
margin-bottom: 15px;
}
p {
font-size: 18px;
color: #666;
margin-bottom: 30px;
line-height: 1.6;
}
</style>
</head>
<body>
<div class="error-container">
<div class="error-code">404</div>
<h1>Page Not Found</h1>
<p>Sorry, the page you're looking for doesn't exist or has been moved.</p>
</div>
</body>
</html>
2.2 Create S3 bucket stack
We create S3 bucket to host our frontend files
import * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as s3deploy from 'aws-cdk-lib/aws-s3-deployment';
import { Construct } from 'constructs';
export interface S3BucketsStackProps extends cdk.StackProps {
bucketNames: string[];
enableWebsiteHosting?: boolean;
websiteIndexDocument?: string;
websiteErrorDocument?: string;
}
export class S3BucketsStack extends cdk.Stack {
public readonly buckets: Map<string, s3.Bucket> = new Map();
public websiteBucket?: s3.Bucket;
constructor(scope: Construct, id: string, props: S3BucketsStackProps) {
super(scope, id, props);
const enableWebsite = props.enableWebsiteHosting ?? false;
const indexDoc = props.websiteIndexDocument ?? 'index.html';
const errorDoc = props.websiteErrorDocument ?? 'error.html';
// Create S3 buckets
props.bucketNames.forEach((bucketName) => {
const bucketConfig: s3.BucketProps = {
bucketName: bucketName,
versioned: false,
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
encryption: s3.BucketEncryption.S3_MANAGED,
};
// Configure for website hosting if enabled
if (enableWebsite) {
Object.assign(bucketConfig, {
publicReadAccess: true,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ACLS,
websiteIndexDocument: indexDoc,
websiteErrorDocument: errorDoc,
});
} else {
Object.assign(bucketConfig, {
publicReadAccess: false,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
});
}
const bucket = new s3.Bucket(this, `${bucketName}-bucket`, bucketConfig);
this.buckets.set(bucketName, bucket);
// Store reference to website bucket
if (enableWebsite && !this.websiteBucket) {
this.websiteBucket = bucket;
}
// Output bucket name
new cdk.CfnOutput(this, `${bucketName}-BucketName`, {
value: bucket.bucketName,
description: `S3 Bucket: ${bucketName}`,
exportName: `${bucketName}-BucketName`,
});
// Output website URL if hosting is enabled
if (enableWebsite) {
new cdk.CfnOutput(this, `${bucketName}-WebsiteURL`, {
value: bucket.bucketWebsiteUrl,
description: `Website URL for ${bucketName}`,
exportName: `${bucketName}-WebsiteURL`,
});
}
});
}
// Helper method to deploy website content
public deployWebsite(sourcePath: string, destinationBucket?: s3.Bucket) {
const targetBucket = destinationBucket ?? this.websiteBucket;
if (!targetBucket) {
throw new Error('No website bucket available for deployment');
}
return new s3deploy.BucketDeployment(this, 'DeployWebsite', {
sources: [s3deploy.Source.asset(sourcePath)],
destinationBucket: targetBucket,
});
}
}
2.3 Create Amazon Bedrock Guardrails
Let’s make our AI-app behaves by using Beckrock Guardrails to filter inappropriate content.
// Create Bedrock Guardrail for content filtering
const guardrail = new bedrock.CfnGuardrail(this, 'SummarizerGuardrail', {
name: `${envName}-summarizer-guardrail`,
description:
'Guardrail for content summarizer to filter inappropriate content',
blockedInputMessaging:
'Sorry, I cannot process this content as it contains inappropriate material.',
blockedOutputsMessaging: 'I apologize, but I cannot provide that summary.',
// Content policy filters
contentPolicyConfig: {
filtersConfig: [
{
type: 'SEXUAL',
inputStrength: 'HIGH',
outputStrength: 'HIGH',
},
{
type: 'VIOLENCE',
inputStrength: 'HIGH',
outputStrength: 'HIGH',
},
{
type: 'HATE',
inputStrength: 'HIGH',
outputStrength: 'HIGH',
},
{
type: 'INSULTS',
inputStrength: 'MEDIUM',
outputStrength: 'MEDIUM',
},
{
type: 'MISCONDUCT',
inputStrength: 'MEDIUM',
outputStrength: 'MEDIUM',
},
{
type: 'PROMPT_ATTACK',
inputStrength: 'HIGH',
outputStrength: 'NONE',
},
],
},
// Topic policy to filter harmful content
topicPolicyConfig: {
topicsConfig: [
{
name: 'HarmfulContent',
definition:
'Content promoting illegal activities, violence, or harmful behavior',
examples: [
'How to make weapons',
'Instructions for illegal activities',
'Content promoting self-harm',
],
type: 'DENY',
},
],
},
});
2.4 Create lambda function
Create IAM role for lambda, make sure it can invoke the model you pick before
// Create IAM role for Lambda function with Bedrock permissions
const lambdaRole = new iam.Role(this, 'SummarizerLambdaRole', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
description:
'Role for content summarizer Lambda function to access Bedrock',
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName(
'service-role/AWSLambdaBasicExecutionRole'
),
],
});
// Add Bedrock invoke permissions including guardrails
lambdaRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['bedrock:InvokeModel', 'bedrock:ApplyGuardrail'],
resources: [
`arn:aws:bedrock:${this.region}::foundation-model/anthropic.claude-3-haiku-20240307-v1:0`,
guardrail.attrGuardrailArn,
],
})
);
Create lambda
// Create Lambda function with Python runtime and automatic dependency bundling
const summarizerFunction = new lambdaPython.PythonFunction(
this,
'SummarizerHandler',
{
runtime: lambda.Runtime.PYTHON_3_12,
entry: path.join(__dirname, '../lambda'),
index: 'summarizer_handler.py',
handler: 'handler',
role: lambdaRole,
timeout: cdk.Duration.seconds(30),
environment: {
GUARDRAIL_ID: guardrail.attrGuardrailId,
GUARDRAIL_VERSION: 'DRAFT',
},
description:
'Lambda function to summarize content using Amazon Bedrock with Guardrails',
}
);
Since our Lambda uses Python and dependencies listed in requirements.txt, you need Docker up and running on your machine. Docker is used by the CDK to bundle the dependencies into a deployment package that Lambda can run. For more details, check out aws-cdk/aws-lambda-python-alpha module.
2.5 Create API Gateway
// Create API Gateway REST API
const api = new apigateway.RestApi(this, 'SummarizerApi', {
restApiName: `${envName}-summarizer-API`,
description: 'API for content summarization',
defaultCorsPreflightOptions: {
allowOrigins: apigateway.Cors.ALL_ORIGINS,
allowMethods: apigateway.Cors.ALL_METHODS,
allowHeaders: [
'Content-Type',
'X-Amz-Date',
'Authorization',
'X-Api-Key',
],
},
});
// Create Lambda integration
const summarizerIntegration = new apigateway.LambdaIntegration(
summarizerFunction
);
// Add POST method to the API
api.root.addMethod('POST', summarizerIntegration);
// Output the API endpoint URL
new cdk.CfnOutput(this, 'ApiEndpoint', {
value: api.url,
description: 'API Gateway endpoint URL for the content summarizer',
exportName: 'SummarizerApiEndpoint',
});
2.6 Lambda function handler
In folder lambda create requirements.txt
boto3>=1.28.0
botocore>=1.31.0
And summarizer_handler.py
import json
import os
import boto3
from botocore.exceptions import ClientError
# Initialize Bedrock client
bedrock_runtime = boto3.client(
'bedrock-runtime', region_name=os.environ.get('AWS_REGION', 'us-east-1')
)
# System prompt to guide the AI to summarize content
SYSTEM_PROMPT = """You are an expert content summarizer. Your task is to create concise summaries that are significantly shorter than the original text.
Guidelines:
- Reduce the content to at least 20-30% of its original length
- Extract only the main ideas and most important key points
- Remove redundant information and examples
- Use clear, direct language
- Maintain objectivity and accuracy
- Format as a coherent paragraph or bullet points as appropriate
IMPORTANT: Your summary must be substantially shorter than the input. Do not repeat or paraphrase the entire content."""
def detect_content_type(content: str) -> str:
"""Simple content type detection based on length and structure"""
word_count = len(content.split())
if word_count < 50:
return 'Short Text'
elif word_count < 200:
return 'Medium Text'
elif word_count < 500:
return 'Long Text'
else:
return 'Extended Text'
def handler(event, context):
"""Lambda handler function"""
print(f'Received event: {json.dumps(event)}')
# Handle CORS preflight
if event.get('httpMethod') == 'OPTIONS':
return {
'statusCode': 200,
'headers': {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
},
'body': json.dumps({}),
}
try:
# Parse request body
if isinstance(event.get('body'), str):
body = json.loads(event['body'])
else:
body = event.get('body', {})
content = body.get('content', '').strip()
if not content:
return {
'statusCode': 400,
'headers': {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
},
'body': json.dumps({'error': 'Content is required'}),
}
# Detect content type
content_type = detect_content_type(content)
# Construct a concise prompt for Claude
user_prompt = f"""Summarize the following text in 3-5 clear sentences. Focus only on the main points and key takeaways.
Text:
{content}"""
# Use Claude 3 Haiku - fast, cost-effective, supports guardrails
model_id = 'anthropic.claude-3-haiku-20240307-v1:0'
# Get guardrail configuration from environment
guardrail_id = os.environ.get('GUARDRAIL_ID')
guardrail_version = os.environ.get('GUARDRAIL_VERSION', 'DRAFT')
# Prepare the request body for Claude
request_body = {
'anthropic_version': 'bedrock-2023-05-31',
'max_tokens': 300,
'temperature': 0.3,
'messages': [
{
'role': 'user',
'content': user_prompt,
},
],
}
# Invoke the model with guardrails
invoke_params = {
'modelId': model_id,
'contentType': 'application/json',
'accept': 'application/json',
'body': json.dumps(request_body),
}
# Add guardrail if configured
if guardrail_id:
invoke_params['guardrailIdentifier'] = guardrail_id
invoke_params['guardrailVersion'] = guardrail_version
print(
f'Using guardrail: {guardrail_id} version {guardrail_version}'
)
response = bedrock_runtime.invoke_model(**invoke_params)
# Parse the response for Claude model
response_body = json.loads(response['body'].read())
print(f'Bedrock response: {json.dumps(response_body)}')
# Extract summary from Claude response
if 'content' in response_body and len(response_body['content']) > 0:
summary = response_body['content'][0]['text'].strip()
else:
raise Exception(f'Unexpected response format: {response_body}')
chat_response = {
'summary': summary,
'contentType': content_type,
'wordCount': len(content.split()),
}
return {
'statusCode': 200,
'headers': {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
},
'body': json.dumps(chat_response),
}
except ClientError as error:
error_code = error.response.get('Error', {}).get('Code', '')
error_message = error.response.get('Error', {}).get(
'Message', str(error)
)
print(f'Error processing request: {error}')
# Handle guardrail intervention
if (
error_code == 'ValidationException'
and 'guardrail' in error_message.lower()
):
return {
'statusCode': 400,
'headers': {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
},
'body': json.dumps(
{
'error': 'Content blocked by guardrail',
'message': 'Sorry, I cannot process this content as it may contain inappropriate material.',
}
),
}
# Handle Bedrock access errors
if error_code == 'AccessDeniedException':
return {
'statusCode': 403,
'headers': {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
},
'body': json.dumps(
{
'error': 'Bedrock access denied. Please ensure Bedrock is enabled in your AWS account and the model is available in your region.',
'details': error_message,
}
),
}
return {
'statusCode': 500,
'headers': {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
},
'body': json.dumps(
{'error': 'Internal server error', 'details': error_message}
),
}
except Exception as error:
import traceback
error_trace = traceback.format_exc()
print(f'Error processing request: {error}')
print(f'Traceback: {error_trace}')
return {
'statusCode': 500,
'headers': {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
},
'body': json.dumps(
{'error': 'Internal server error', 'details': str(error)}
),
}
2.6 Update aws-cdk-summarizer.ts
import * as cdk from 'aws-cdk-lib';
import { getAccountId } from '../lib/utils';
import { AwsCdkSummarizerStack } from '../lib/aws-cdk-summarizer-stack';
import { S3BucketsStack } from '../lib/s3-buckets-stack';
const configFolder = '../config/';
const accountFileName = 'aws_account.yaml';
// Define common tags
const commonTags = {
createdby: 'KateVu',
createdvia: 'AWS-CDK',
repo: 'https://github.com/',
};
// Function to apply tags to a stack
function applyTags(stack: cdk.Stack, tags: Record<string, string>): void {
Object.entries(tags).forEach(([key, value]) => {
cdk.Tags.of(stack).add(key, value);
});
}
//Set up default value
const envName = process.env.ENVIRONMENT_NAME || 'kate';
const accountName = process.env.ACCOUNT_NAME || 'sandpit2';
const region = process.env.REGION || 'ap-southeast-2';
const aws_account_id = process.env.AWS_ACCOUNT_ID || 'none';
//Get aws account id
let accountId = aws_account_id;
if (aws_account_id == 'none') {
accountId = getAccountId(accountName, configFolder, accountFileName);
}
const app = new cdk.App();
const bucketNames = [`${envName}-bedrock-summarizer-app`];
const s3BucketsStack = new S3BucketsStack(app, 'S3BucketsStack', {
stackName: `aws-cdk-summarizer-s3-${envName}`,
bucketNames: bucketNames,
enableWebsiteHosting: true,
websiteIndexDocument: 'index.html',
websiteErrorDocument: 'error.html',
env: {
account: accountId,
region: region,
},
});
// Deploy error.html to the website bucket
s3BucketsStack.deployWebsite('./frontend');
const awsCdkSummarizerStack = new AwsCdkSummarizerStack(
app,
'AwsCdkSummarizerStack',
{
/* If you don't specify 'env', this stack will be environment-agnostic.
* Account/Region-dependent features and context lookups will not work,
* but a single synthesized template can be deployed anywhere. */
/* Uncomment the next line to specialize this stack for the AWS Account
* and Region that are implied by the current CLI configuration. */
// env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
/* Uncomment the next line if you know exactly what Account and Region you
* want to deploy the stack to. */
stackName: `aws-cdk-summarizer-${envName}`,
region: region,
accountId: accountId,
accountName: accountName,
envName: envName,
/* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */
}
);
awsCdkSummarizerStack.addDependency(s3BucketsStack);
// Apply tags to both stacks
applyTags(s3BucketsStack, {
...commonTags,
environment: envName,
});
applyTags(awsCdkSummarizerStack, {
...commonTags,
environment: envName,
});
2.7 Deploy the app
- Ensure valid credential for the target AWS account
- Export environment variable or the app will use the ones have been set in aws-cdk-summarizer.ts
- Run
cdk deploy -- allto deploy both stacks
2.8 Update the API endpoint in index.html file
- Update the API endpoint in index.html file with the API endpoint (can see as output when deploying the stacks)
// TODO: Replace with your actual API Gateway endpoint after deployment
const API_ENDPOINT = 'YOUR_API_ENDPOINT_HERE';
- Re-upload index.html to S3 bucket
Test your app
Go to S3 bucket, check tab Properties to get the bucket S3 endpoint

Go to the website, put some text and check the result

You can tweak the configuration and the user_prompt to in your Lambda function to fine-tune how the summaries are generated
# Prepare the request body for Claude
request_body = {
'anthropic_version': 'bedrock-2023-05-31',
'max_tokens': 300,
'temperature': 0.3,
'messages': [
{
'role': 'user',
'content': user_prompt,
},
],
} # Prepare the request body for Claude
request_body = {
'anthropic_version': 'bedrock-2023-05-31',
'max_tokens': 300,
'temperature': 0.3,
'messages': [
{
'role': 'user',
'content': user_prompt,
},
],
}
Final Thoughts
And that’s it, an AI-powered text summarizer running an Amazon Bedrock, protected by Bedrock Guardrails, and served through S3 bucket.
For this experiment, Kiro has been a great companion, making developing, testing much smoother.
From here you can tweak the prompts to change it to general chatbot, try different models or extend the app to handle multiple languages, …



Top comments (0)