Sentiment Analysis with AWS Comprehend | AI/ML Series

In the last post we discussed on how to add speaking ability to our applications using AWS Polly. Let’s extend the same example to analyze the sentiments of the text that user types.

As usual, I recommend to watch the following video before reading this blogpost and use this post as a reference when building out the application by your own.

AWS Comprehend Service

AWS comprehend uses NLP to extract the insight about the content without needing any preprocessing requirements. It is capable of recognizing Entities, Languages, Sentiments, Key Phrases and other common elements of the given text or the document. One of the common use case of AWS Comprehend is to analyze the social media feed about your product and take necessary actions upon analyzing users valid sentiments.

Calling Comprehend API Methods

Let’s use AWS Lambda, our serverless function to talk to AWS Comprehend service and do a sentiment analysis. We are going to be using the API methods detectSentiment and detectDominantLanguage from AWS Comprehend javascript SDK. Refer the full SDK documentation here.

Firstly, we are creating an endpoint that triggers the Lambda function. Goto your serverless.yml and add this piece of code.

functions:
analyze:
handler: handler.analyze
events:
- http:
path: analyze
method: post
cors: true

It will create a new endpoint in the API Gateway with the path /analyze that will trigger analyze Lambda function. Here is the analyze function code which needs to be in the handler.js.

module.exports.analyze = (event, context, callback) => {
let body = JSON.parse(event.body);

const params = {
Text: body.text
};

// Detecting the dominant language of the text
comprehend.detectDominantLanguage(params, function (err, result) {
if (!err) {
const language = result.Languages[0].LanguageCode;

const sentimentParams = {
Text: body.text,
LanguageCode: language
};

// Analyze the sentiment
comprehend.detectSentiment(sentimentParams, function (err, data) {
if (err) {
callback(null, {
statusCode: 400,
headers: {
"Access-Control-Allow-Origin": "*"
},
body: JSON.stringify(err)
});
} else {
callback(null, {
statusCode: 200,
headers: {
"Access-Control-Allow-Origin": "*"
},
body: JSON.stringify(data)
});
}
});
}
});
}

At the top of the handler function, you need a reference to the Comprehend API from AWS-SDK. Then let’s first identify the dominant language of the text by calling detectDominantLanguage API method and pass that language code to the next API call detectSentiment inside the callback of the first method.

As a result, you will get the matching Sentiment and the matching percentage of Negative, Positive, Neutral and Mixed sentiment. Now, send that back to the frontend.

IAM Permission for AWS Comprehend

We are now almost finished with the backend, except we have to add a policy that allows AWS Comprehend permission to the IAM role attached to the Lambda function. If you haven’t read the part 01 of this series, read/watch it where I showed you how to setup an IAM role for the lambda.

Our IAM role was youtube-polly-actual-role. It had an arn and we refereed it in the serverless.yml file as follows.

arn:aws:iam::<account-id>:role/youtube-polly-actual-role

Goto IAM console of your AWS account and attach a new policy to that same role as shown below.

Setting up the Frontend

We have been using an Angular app as the frontend in the earlier project. Let’s continue adding a button below the user text area and call our API endpoint.

Goto app.component.html and add this simple html code to display an additional button next to “speak” button. We will display the returned sentiment value with a color below the button as well.

<div style="margin: auto; padding: 10px; text-align: center;">
<h2>Write Something...</h2>
<div>
<textarea #userInput style="font-size: 15px; padding: 10px;" cols="60" rows="10"></textarea>
</div>
<div>
<select [(ngModel)]="selectedVoice">
<option *ngFor="let voice of voices" [ngValue]="voice">{{voice}}</option>
</select>
</div>
<div style="margin-top: 10px">
<button style="font-size: 15px;" (click)="speakNow(userInput.value)">Speak Now</button>
<button style ="font-size: 15px;" (click)="analyze(userInput.value)">Analyze</button>
</div>

<!-- Following section will show the returned sentiment value with a suitable color -->

<div>
<h2 *ngIf="sentiment=='POSITIVE'" style="color: green;">{{sentiment}}! </h2>
<h2 *ngIf="sentiment=='NEUTRAL'" style="color: orange;">{{sentiment}} </h2>
<h2 *ngIf="sentiment=='NEGATIVE'" style="color: red;">{{sentiment}}! </h2>
</div>
</div>

Let’s add the analyze function in the app.compotent.ts file and make use of a service to call the API Gateway Endpoint.

import { Component } from '@angular/core';
import { APIService } from './api.service'

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})

export class AppComponent {
sentiment = null;
constructor(private api: APIService){}

analyze(input) {
let
data = {
text: input
}
this
.api.analyze(data).subscribe((result:any) => {
this
.sentiment = result.Sentiment;
});
}

}

Let’s call frontend API service to call the /analyze endpoint and return the data. Goto api.service.ts and add this code.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({
providedIn: 'root'
})
export class APIService {

ENDPOINT = 'https://461xegl8zf.execute-api.us-east-1.amazonaws.com/dev';

constructor(private http:HttpClient) {}

speak(data) {
return this.http.post(this.ENDPOINT + '/speak', data);
}

analyze(data) {
return this.http.post(this.ENDPOINT + '/analyze', data);
}
}

Our frontend is now completed. It will send the user input to the backend endpoint and lambda function will figure out the language of the text and send sentiment analysis.

Result

Cheers!

Please follow and like us:

Building a Profile App – Part 02

In Part 01 we started building the Profile app with Amplify as the frontend library. We managed to save the user information on a DynamoDB table via a GraphQL API.

In this second part, let’s add following features and improvements to our app.

  1. Securely uploading the profile image
  2. Loading the saved user data
  3. Implement an auth guard for the profile page to avoid unauthorized access
  4. Automatically redirect to the profile page after a successful login

Configuring Storage Category with Amplify

For the sake of this application let’s allow a user to view only the profile picture of himself. (May be it makes no sense, but I want to show how to use private images using amplify storage service).

Okay, let’s use two higher order components from Amplify Angular Library to make this task very easy.

  • <amplify-photo-picker></amplify-photo-picker>
  • <amplify-s3-image></amp

amplify-photo-picker allows users to upload image to S3. We can pass different storage options for our liking. It supports three storage options i.e. public, protected and private. We are going to use private level that only allows the owner to view and upload the image.

But hey, before that let’s add Storage category with amplify that will create a S3 bucket for us. So, get a command prompt and run following commands.

amplify add storage

? Please select from one of the below mentioned services:
Content (Images, audio, video, etc.)
? Please provide a friendly name for your resource that will be used to label this category in the project: s38e43106 a
? Please provide bucket name: profileapp03f4977230524d1e977654540b6c1924
? Who should have access: Auth users only
? What kind of access do you want for Authenticated users: read/write

amplify push

Securely uploading the profile image

Now let’s bring on those two components to the profile.component.html

 <h2>My Profile</h2>
<div class="form-group row">
<div class="col-sm-12">
<div class="md-form mt-0">
<mdb-icon *ngIf="showPhoto" fas icon="upload" (click)="editPhoto()" size="2x" class="upload-icon"></mdb-icon>
<!-- Display Image -->
<amplify-s3-image [path]="user.imageUrl"
[options]="{'level': 'private'}" *ngIf="showPhoto">
</amplify-s3-image>

<!-- Photo Picker -->
<amplify-photo-picker *ngIf="!showPhoto"
path="image"[storageOptions]="{'level': 'private'}" (uploaded)="onImageUploaded($event)">
</amplify-photo-picker>
</div>
</div>
</div>
<form> ...

Edit the profile.component.ts as follows.

export class ProfileComponent implements OnInit {
...
showPhoto: boolean;
userCreated: boolean;

async onImageUploaded(e) {
this.user.imageUrl = e.key;
if (this.userCreated) {
await this.api.UpdateUser({
id: this.userId,
image: this.user.imageUrl
});
}
this.showPhoto = true;
}

editPhoto() {
this.showPhoto = false;
}

getType(): string {
return this.userCreated ? 'UpdateUser' : 'CreateUser';
}

async updateProfile() {
const user = {
id: this.userId,
username: this.user.firstName + '_' + this.user.lastName,
firstName: this.user.firstName,
lastName: this.user.lastName,
bio: this.user.aboutMe,
image: this.user.imageUrl
}
await this.api[this.getType()](user);
}
...
}

Loading Saved Data

At this point our application is managed to store profile information on DynamoDB table and store the profile image on a S3 bucket. However, when we reload the webpage, all information is disappeared. Let’s fix that by fetching the saved data upon profile component loading.

We are going to update ngOnInit lifecycle method to load up the user data and populate User model which will automatically bind to our angular form.

...  
ngOnInit() {
this.showPhoto = false;
Auth.currentAuthenticatedUser({
bypassCache: false
}).then(async user => {
this.userName = user.username;
this.userId = user.attributes.sub;
let result = await this.api.GetUser(this.userId);
if (!result) {
this.userCreated = false;
this.user = new User('', '', '', '', '', '');
} else {
this.userCreated = true;
this.showPhoto = !!result.image;
this.user = new User(
this.userId,
result.username,
result.firstName,
result.lastName,
result.bio,
result.image
)
}
})
.catch(err => console.log(err));
}
...

Logout Functionality

Now that we have almost finished with the profile app functionalities, let’s add a method to logout for authenticated users.

In profile.component.ts file add the following method that calls the signOut method of Auth api.

import { Router } from '@angular/router';
...
constructor(private api: APIService, private router: Router) {}
...
logOut() {
Auth.signOut({ global: true })
.then(data => {
this.router.navigate(['/auth']);
})
.catch(err => console.log(err));
}

Make sure to bind this function for the click event of the Logout link in the template.

Configuring Auth Guards

Currently, we have two basic routes. One for the login screen and the other for our profile component. We must not allow to load profile component unless the user is logged in. We can achieve that using an auth guard.

Create an auth guard with,

ng g guard auth

Here is the code for auth guard service.

import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { Auth } from 'aws-amplify';

@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(private router: Router) {}

canActivate(): Promise < boolean > {
return new Promise((resolve) => {
Auth.currentAuthenticatedUser({
bypassCache: false
})
.then((user) => {
if(user){
resolve(true);
}
})
.catch(() => {
this.router.navigate(['/login']);
resolve(false);
});
});
}
}

Auth.currentAuthenticatedUser Api call always returns the currently authenticated user. If there is no currently authenticated user Auth guard will be resolved as false and Profile component will not be activated.

So now, let’s add that auth guard to guard our Profile component in app-routing.module.js

import { AuthGuard } from './auth.guard';
...
const routes: Routes = [{
path: "profile",
component: ProfileComponent,
canActivate: [AuthGuard]
},
{
path: "login",
component: AuthComponent
},
{
path: '**',
redirectTo: 'login',
pathMatch: 'full'
}
];

Automatic Redirection After Login

Finally, let’s add automatic redirection to profile page once a user is successfully authenticated. We can accomplish this by listening to authStateChange$ events generated by Amplify library.

Goto auth.component.js file and add following code.

import { AmplifyService } from 'aws-amplify-angular';
import { Router } from '@angular/router';

constructor(public amplifyService: AmplifyService, public router: Router) {
this.amplifyService = amplifyService;
this.amplifyService.authStateChange$
.subscribe(authState => {
if (authState.state === 'signedIn') {
this.router.navigate(['/profile']);
}
});
}

Okay. Now we can run the application and check if everything works. Login with a registered user. Make sure you will be redirected to the Profile page. Then update the profile information with a profile image and make sure the information is persisted.

ng serve

You will still see the Amplify sign-In page for a second before the redirection. In order to hide that default component pass the “hide” input to <amplify-authenticator>

<amplify-authenticator [hide]="['Greetings']"></amplify-authenticator>

Final Page

I hope this post has been useful. Please find the github repo of this example project at https://github.com/mjzone/amplify-user-profile

Cheers!

Please follow and like us:

Building a Profile App – Part 01

This blog post is connected to the following youtube video. I would recommend you to watch the video first and use this blog post to copy the code snippets and build the application by yourself.

Watch the video here

Creating the Angular App

Let’s start a new angular application using ng new command.

ng new profileApp

Select YES for angular routing and select SCSS when prompted from the CLI. After the project is created, change directory into the profileApp folder by,

cd profileApp

Now let’s create two components for Login page and Profile landing page.

ng g c auth
ng g c profile

Installing Amplify Libraries

It’s time to add amplify and aws-appsync libraries. Firstly, install the amplify cli globally and configure it with your AWS account.

npm install -g @aws-amplify/cli
amplify configure

Afterwards, we need to install amplify, amplify-angular, app-sync and graphql-tag libraries as we are to use them in our profile app.

npm install --save aws-amplify
npm install --save aws-amplify-angular

Additional configuration for the Angular App

We need to add some polyfills and additional configurations to get amplify and appsync work with our angular application. Otherwise you’ll waste much time troubleshooting errors.

In the polyfills.ts file (src/polyfills.ts) add following two lines on top of the file.

(window as any).global = window; 
(window as any).process = { browser: true };

Also goto index.html (src/index.html) and add the following script within the head tags.

<script>
if(global === undefined) {
var global = window;
}
</script>

Now goto tsconfig.app.json (src/tsconfig.app.json) and add “node” as the compilerOptions type.

{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"types": ["node"]
},
"exclude": [
"test.ts",
"**/*.spec.ts"
]
}

Initializing an Amplify Project on Cloud

At this point, we can initialize an amplify project using the amplify cli.

amplify init
## Provide following answers when prompted

? Enter a name for the project profileApp
? Enter a name for the environment dev
? Choose your default editor: Visual Studio Code
? Choose the type of app that you're building javascript
Please tell us about your project? What javascript framework are you using angular
? Source Directory Path: src
? Distribution Directory Path: dist/profileApp
? Build Command: npm run-script build
? Start Command: ng serve
## Choose your aws profile when prompted as well

After the process is completed, let’s add two amplify categories for auth and api.

amplify add auth
## Provide following answer for the prompt

Do you want to use the default authentication and security configuration? Yes, use the default configuration.
amplify add api
## Provide following answers for the prompts

? Please select from one of the below mentioned services GraphQL
? Provide API name: profileapp
? Choose an authorization type for the API Amazon Cognito User Pool
Use a Cognito user pool configured as a part of this project
? Do you have an annotated GraphQL schema? No
? Do you want a guided schema creation? Yes
? What best describes your project: Single object with fields (e.g., “Todo” with ID, name, description)
? Do you want to edit the schema now? Yes

Now, amplify will open the schema.graphql file with sample model. While command prompt is open, replace the content with the following graphql model and save the file and press enter to continue in the command prompt.

type User @model {
id: ID!
username: String!
firstName: String
lastName: String
bio: String
image: String
}

At this point, we have created the templates for all the AWS resources locally. We need to push the template and actually create the services. To do that type,

amplify push
## Provide following answers when prompted

? Are you sure you want to continue? Yes
? Do you want to generate code for your newly created GraphQL API Yes
? Choose the code generation language target angular
? Enter the file name pattern of graphql queries, mutations and subscriptions src/graphql/*/.graphql
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes
? Enter maximum statement depth [increase from default if your schema is deeply nested] 2
? Enter the file name for the generated code src/app/API.service.ts

It will take few minutes to provision the resources on AWS. Be patient 🙂

Configuring Amplify Libraries with the App

Now that we have configured the resources on AWS, it creates a new file i.e. aws-exports.js file with all the configuration details of those services in the frontend directory structure.

Let’s use that file and setup the initiate connection from Angular frontend to AWS backend.

Goto main.ts (/src/main.ts) file and configure amplify.

import Amplify from 'aws-amplify';
import amplify from './aws-exports';

Amplify.configure(amplify);

Now let’s import amplify-angular library to use the already configured higher order components for our login.

Goto app.module.js and import AmplifyAngularModule and AmplifyService.

import {AmplifyAngularModule, AmplifyService} from 'aws-amplify-angular';

@NgModule({
declarations: [
AppComponent
...
],
imports: [
...
AmplifyAngularModule
],
providers: [AmplifyService]
})

Now we can use <amplify-authenticator></amplify-authenticator> component directly in the auth component html and implement a complete login functionality. (Magical!)

But before that let’s setup our routes in app-routing.module.js file. We have two basic routes. One for the login screen and the other for our profile component.

In the app-routing.module.ts file add the routes.

 const routes: Routes = [{
path: "profile",
component: ProfileComponent
},
{
path: "login",
component: AuthComponent
},
{
path: '**',
redirectTo: 'login',
pathMatch: 'full'
}
];

Adding the Login Component

It’s time to add the login screen. Goto auth.component.html and add this code. It will turn in to a login screen.

<amplify-authenticator></amplify-authenticator>

Before running the application to check login screen, you need to update the styles in of amplify-authenticator in styles.scss file.

Add this line of css in the style.scss file (src/styles.scss)

@import '~aws-amplify-angular/theme.css';

We need to remove the default content that angular has added in app.component.html file. So let’s do that too. Your app.component.html should look like this, when you remove the default code

<router-outlet></router-outlet>

Okay. Now let’ run ng serve and check the output!

Figure 01 – Login Page

Styling with MDBootStrap

Now we need to build the Profile component. But before that, let’s configure MDBootStrap with our project to add styles to the profile component easily.

npm i angular-bootstrap-md --save

npm install -–save chart.js@2.5.0 @types/chart.js @types/chart.js @fortawesome/fontawesome-free hammerjs 

To app.module.ts add,

import { MDBBootstrapModule } from 'angular-bootstrap-md'; 

@NgModule({ imports: [ MDBBootstrapModule.forRoot() ] });

In the angular.json file replace styles and scripts sections with,

"styles": [
"node_modules/@fortawesome/fontawesome-free/scss/fontawesome.scss",
"node_modules/@fortawesome/fontawesome-free/scss/solid.scss",
"node_modules/@fortawesome/fontawesome-free/scss/regular.scss",
"node_modules/@fortawesome/fontawesome-free/scss/brands.scss",
"node_modules/angular-bootstrap-md/scss/bootstrap/bootstrap.scss",
"node_modules/angular-bootstrap-md/scss/mdb-free.scss",
"src/styles.scss"
],
"scripts": [
"node_modules/chart.js/dist/Chart.js",
"node_modules/hammerjs/hammer.min.js"
]

Adding the Profile Component

Now let’s edit the profile component. In profile.component.html, add following html code,

<!-- Navigation Bar -->
<header>
<nav class="navbar navbar-expand-lg navbar-dark default-color">
<a class="navbar-brand" href="#"><strong>Profile</strong></a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" href="#"> Hello {{userName}}!</a>
</li>
<li class="nav-item active">
<a class="nav-link"> Logout <span class="sr-only">(current)</span></a>
</li>
</ul>
</div>
</nav>
</header>

<!-- Main Content -->
<main class="text-center my-5">
<div class="container">
<h2>My Profile</h2>
<form>
<div class="form-group row">
<label for="firstName" class="col-sm-2 col-form-label">First Name</label>
<div class="col-sm-10">
<div class="md-form mt-0">
<input type="text" class="form-control" id="firstName" name="firstName" [(ngModel)]="user.firstName">
</div>
</div>
</div>
<div class="form-group row">
<label for="lastName" class="col-sm-2 col-form-label">Last Name</label>
<div class="col-sm-10">
<div class="md-form mt-0">
<input type="text" class="form-control" id="lastName" name="lastName" [(ngModel)]="user.lastName">
</div>
</div>
</div>
<div class="form-group row">
<label for="aboutMe" class="col-sm-2 col-form-label">About Me</label>
<div class="col-sm-10">
<div class="md-form mt-0">
<textarea id="aboutMe" name="aboutMe" [(ngModel)]="user.aboutMe" class="form-control md-textarea" length="120"
rows="3"></textarea>
</div>
</div>
</div>
<div class="form-group row">
<div class="col-sm-3">
<button type="submit" class="btn btn-primary btn-lg" (click)="updateProfile()">Update</button>
</div>
</div>
</form>
</div>
</main>

In the form, we data binding to a model called user. Let’s add that model and import it to the profile.component.ts file.

// Generate a typescript class
ng g class User

Add following code to user.ts,

Since we need to use ngModel in the profile component, we should import FormsModule into the app.module.js

import { FormsModule} from '@angular/forms';

@NgModule({
imports: [
FormsModule,
...
]
)

Okay, now we need to implement updateProfile() function to grab the data from the form and store the data in the DynamoDB table.

export class User {
constructor(
public id: string,
public username: string,
public firstName: string,
public lastName: string,
public aboutMe: string,
public imageUrl: string
){}
}

In the profile.component.js file add,

import { Component, OnInit } from '@angular/core';
import { APIService } from '../API.service';
import { User } from '../user';
import { Auth } from 'aws-amplify';

@Component({
selector: 'app-profile',
templateUrl: './profile.component.html',
styleUrls: ['./profile.component.scss']
})

export class ProfileComponent implements OnInit {
userId: string;
userName: string;
user = new User('', '', '', '', '', '');

constructor(private api: APIService) {}

ngOnInit() {
Auth.currentAuthenticatedUser({
bypassCache: false
}).then(async user => {
this.userId = user.attributes.sub;
this.userName = user.username;
})
.catch(err => console.log(err));
}

async updateProfile() {
const user = {
id: this.userId,
username: this.user.firstName + '_' + this.user.lastName,
firstName: this.user.firstName,
lastName: this.user.lastName,
bio: this.user.aboutMe
}
await this.api.CreateUser(user);
}
}

updateProfile function will get the firstName, userName, lastName, and bio information from the form inputs.

But the “id” attribute has to be taken from currently authenticated user.

Now, let’s run ng serve and goto “/profile” path to view our profile page.

Figure 02 – Profile Page

In the second part of this blog we are going to add following functionalities to our profile app.

  • Loading the saved user data
  • Ability to securely upload the profile image
  • Adding auth guard for the profile component so that unauthorized users will not have access to profile page
  • Automatically redirecting to profile page after a successful login

So guys, I hope this has been useful to you. I’ll see you in the next part.

Stay tuned!

Please follow and like us: