DEV Community

Cover image for Build a Smart Chatbot with AWS Lambda, Lex, and Enhanced Sentiment Analysis - (Let's Build 🏗️ Series)
awedis for AWS Heroes

Posted on

Build a Smart Chatbot with AWS Lambda, Lex, and Enhanced Sentiment Analysis - (Let's Build 🏗️ Series)

Let's say you run a company with a customer assistance call center or a website where clients give feedback about your services. After operating for a while your company starts to scale up, more traffic, hence more users sharing their feedback. So it's the right time to begin implementing chatbots and even integrating them with AI.

Alright without further let's jump in. In this article, I will show you how you can create such smart Chatbots using AWS services. Our chatbot will be smart to receive messages from clients, then it will process the user tone, and based (on positive, negative, or neutral) it will categorize the messages using Amazon Comprehend for sentiment analysis.

The main parts of this article:
1- About AWS Services
2- Architecture overview (Terraform)
3- Technical Part (GO code)
4- Result
5- Conclusion

About AWS Services

1- Amazon Lex to handle the chatbot interactions.

To learn more about Amazon Lex: Official Page

2- Amazon Comprehend for sentiment analysis.

To learn more about Amazon Comprehend: Official Page

3- AWS Lambda will coordinate the process by taking input, querying Lex, analyzing the sentiment with Comprehend, and categorizing the message.

To learn more about AWS Lambda: Official Page

Architecture Overview

Our architecture will be serverless, mainly everything will be done event-driven however you can implement something similar using containers.

terraform {
  required_version = "1.5.1"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.70.0"
    }
  }
}

provider "aws" {
  region = var.region
}

resource "aws_iam_role" "lambda_execution_role" {
  name = "lets-build-lambda-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Action = "sts:AssumeRole",
        Effect = "Allow",
        Principal = {
          Service = "lambda.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_iam_policy" "lambda_lex_comprehend_policy" {
  name = "lambda_lex_comprehend_policy"
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "lex:PostText",
          "comprehend:DetectSentiment",
          "logs:*"
        ]
        Resource = "*"
        Effect   = "Allow"
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "lambda_policy_attachment" {
  role       = aws_iam_role.lambda_execution_role.name
  policy_arn = aws_iam_policy.lambda_lex_comprehend_policy.arn
}

resource "aws_lambda_function" "chatbot_function" {
  filename         = "./bootstrap.zip"
  function_name    = "lets-build-function"
  handler          = "main"
  role             = aws_iam_role.lambda_execution_role.arn
  memory_size      = "128"
  timeout          = "3"
  source_code_hash = filebase64sha256("./bootstrap.zip")
  runtime          = "provided.al2"
  architectures    = ["arm64"]

  environment {
    variables = {
      LEX_BOT_NAME = "Chatbot"
      LEX_BOT_ALIAS = "Prod"
      REGION = "${var.region}"
    }
  }
}

resource "aws_lambda_permission" "lex_lambda_invocation" {
  statement_id  = "AllowLexInvocation"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.chatbot_function.arn
  principal     = "lex.amazonaws.com"
}
Enter fullscreen mode Exit fullscreen mode
resource "aws_lex_bot" "chatbot" {
  name                      = "Chatbot"
  child_directed            = false
  process_behavior          = "BUILD"
  enable_model_improvements = true
  detect_sentiment          = true

  intent {
    intent_name    = aws_lex_intent.customer_intent.name
    intent_version = aws_lex_intent.customer_intent.version
  }

  abort_statement {
    message {
      content_type = "PlainText"
      content      = "Sorry, I am not able to assist you at this time"
    }
  }
}

resource "aws_lex_intent" "customer_intent" {
  name    = "ExampleIntent"

  sample_utterances = [
    "I need help",
    "Something is not working",
    "Please assist me",
  ]

  fulfillment_activity {
    type = "ReturnIntent"
  }
}

resource "aws_lex_bot_alias" "prod" {
  name    = "Prod"
  bot_name = aws_lex_bot.chatbot.name
  bot_version = aws_lex_bot.chatbot.version
}

output "lex_bot_arn" {
  value = aws_lex_bot.chatbot.arn
}
Enter fullscreen mode Exit fullscreen mode

Image description

Technical Part (GO code)

package main

import (
    "context"
    "log"
    "strings"

    "github.com/aws/aws-lambda-go/lambda"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/comprehend"
    "github.com/aws/aws-sdk-go/service/lexruntimeservice"
)

type ChatInput struct {
    Message string `json:"message"`
    UserID  string `json:"user_id"`
}

type ChatOutput struct {
    ResponseMessage string `json:"response_message"`
    Category        string `json:"category"`
}

var lexClient *lexruntimeservice.LexRuntimeService
var comprehendClient *comprehend.Comprehend

func handler(ctx context.Context, chatInput ChatInput) (ChatOutput, error) {
    log.Printf("-- hello --")

    sess := session.Must(session.NewSession())
    lexClient = lexruntimeservice.New(sess)
    comprehendClient = comprehend.New(sess)

    lexResp, err := lexClient.PostText(&lexruntimeservice.PostTextInput{
        BotName:    aws.String("Chatbot"),
        BotAlias:   aws.String("Prod"),
        UserId:     aws.String(chatInput.UserID),
        InputText:  aws.String(chatInput.Message),
    })
    if err != nil {
        log.Printf("Error communicating with Lex: %v", err)
        return ChatOutput{}, err
    }

    responseMessage := aws.StringValue(lexResp.Message)

    compResp, err := comprehendClient.DetectSentiment(&comprehend.DetectSentimentInput{
        Text:     aws.String(chatInput.Message),
        LanguageCode: aws.String("en"),
    })
    if err != nil {
        log.Printf("Error detecting sentiment with Comprehend: %v", err)
        return ChatOutput{}, err
    }

    category := categorizeBySentiment(*compResp.Sentiment)

    return ChatOutput{
        ResponseMessage: responseMessage,
        Category:        category,
    }, nil
}

func categorizeBySentiment(sentiment string) string {
    switch strings.ToLower(sentiment) {
    case "positive":
        return "Positive Feedback"
    case "negative":
        return "Critical Feedback"
    case "neutral":
        return "General Inquiry"
    case "mixed":
        return "Mixed Feedback"
    default:
        return "Uncategorized"
    }
}

func main() {
    lambda.Start(handler)
}
Enter fullscreen mode Exit fullscreen mode

Result

Now let's test by triggering our Lambda function. I will input two different JSONs, one for good feedback and one for bad, and we should be able to detect them.

{
  "user_id": "1234",
  "message": "Something is not working, I need help please"
}
Enter fullscreen mode Exit fullscreen mode

As we can see Amazon Comprehend was able to detect the tone.

responseMessage := aws.StringValue(lexResp.Message)
Enter fullscreen mode Exit fullscreen mode

Since in this section I am returning the response directly from the Lex it returned an empty string, but you can add a response there or even edit the Lambda code to return content.

Image description

{
  "user_id": "12345",
  "message": "Thank you, it worked as expected"
}
Enter fullscreen mode Exit fullscreen mode

Here again, it was able to detect that it was positive, but since we had an abort statement as a fallback it returned that content.

Image description

Conclusion

In today's world data is everything, and you always need to collect this data (feedback) from your clients, which will lead to the success of your corporation. I hope my article helped you to achieve something or even added some motivation to your to-do tasks.

Happy coding 👨🏻‍💻

If you'd like more content like this, please connect with me on LinkedIn.

Top comments (0)