DEV Community

Cover image for Building a Summarizer app using Amazon Bedrock and Bedrock Guardrails using AWS CDK
Kate Vu
Kate Vu

Posted on • Originally published at katevu3.Medium

Building a Summarizer app using Amazon Bedrock and Bedrock Guardrails using AWS CDK

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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,
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

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',
          },
        ],
      },
    });
Enter fullscreen mode Exit fullscreen mode

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,
        ],
      })
    );
Enter fullscreen mode Exit fullscreen mode

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',
      }
    );
Enter fullscreen mode Exit fullscreen mode

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',
    });
Enter fullscreen mode Exit fullscreen mode

2.6 Lambda function handler

In folder lambda create requirements.txt

boto3>=1.28.0
botocore>=1.31.0
Enter fullscreen mode Exit fullscreen mode

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)}
            ),
        }
Enter fullscreen mode Exit fullscreen mode

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,
});
Enter fullscreen mode Exit fullscreen mode

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 -- all to 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';
Enter fullscreen mode Exit fullscreen mode
  • 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,
                },
            ],
        }
Enter fullscreen mode Exit fullscreen mode

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, …


References

* Amazon Bedrock Documentation

Top comments (0)