LibertyIT
Published in

LibertyIT

How to build an AWS Lex chatbot for an iOS app

This monster-length post will discuss how to create a basic chatbot using AWS Lex and how to integrate it with an iOS app. We’ll cover everything you’ll need so lots to cover:

  • AWS Lex
  • AWS Lambda (using Node.js)
  • AWS Cognitio & IAM
  • AWS Lex iOS SDK in XCode (Swift)

What will our chatbot do?

We’ll make a chatbot, about chatbots! It’ll answer questions like “What is a chatbot”, “Explain to me about bots” and maybe even “do bots dream of electric sheep” …

Costs

Before you start, take a look at the AWS pricing models, Lex and Lambda are generally free up until a large threshold, so you’re most likely to be ok, but with great power comes great responsibility — and occasionally not-so-great bills, so always good to check before building anything!

Lex

Presuming you’ve an AWS account already, in the console select Lex:

you’ll find it under Artificial Intelligence

Lex is currently only in the US East N. Virginia region aka US-East-1, so you’ll need to select that:

region selection

If it’s your first LEX bot you’ll see a Get Started button, hit that and then on the next page hit Create and you’ll see this:

AWS screenshot

It comes with three templates that that come with code already, along with AWS tutorials to assist, but we’re going to build our bot from scratch so select Custom bot.

Fill in the details as required — I’m calling it “botBot”, as it’s a bot … about bots. I’ve selected the output voice as “Joanna”, along with a completely random piece of text to test the voice. Feel free to try out the different voices with your own text.

When ready hit Create and you’ll see the screen:

We’re now ready to star building the bot, which firstly involves:

Intents and Utterances

Intents are at the core of most chatbots. An Intent is basically the “intent” of a statement or question posed to the chatbot, as oppose to just the question itself. It signifies the “meaning” of the question and you can have many different questions that actually have the same intent.

For example, we want our users to be able to ask “what is a chatbot?”.

The intent of this question is that user wants to understand what a chatbot is i.e an explanation. There are multiple different ways of asking this question:

  • What’s a chatbot
  • What is a chatbot
  • Tell me about chatbots
  • Describe chatbots
  • Explain chatbots
  • I’ve never heard of chatbots
  • What the hell is a chatbot
  • Okay. I was at a wine tasting with my cousin Ernesto. Which was mainly reds, and you know I don’t like reds, man. But there was a rosé that saved the day. It was delightful. But then he told me about chatbots and I don’t know what they are! What are they?

but they all have the same intent.

In Lex, all those different samples of stating the same thing are called utterances.

So, let’s create an intent and give it some utterances: hit the Create Intent button:

AWS screenshot, not an actual button here!

and you’ll see this:

Then hit Create new intent and you’ll be asked to give it a name, I type “ExplainChatbot” and hit Add

It’ll be created and you can start adding sample utterances i.e. questions, for that intent. Just type them in one at a time, hitting enter to add more.

At the bottom of the page, hit Save Intent.

You’ll also see this on the same page:

These options let you specify what Lambda function(s) you want to attach this intent to — you can attach a Lambda for validation (of the user input) and also for fulfillment (answering the user input).

The fulfillment is currently set to Return parameters to client — this basically means that it won’t do anything, but you can test it. First you need to build the bot — hit the Build button in the top righthand corner:

and you’ll see this:

so hit Build again. Once built, you can test the bot using the testing panel:

Where it says Chat to your bot type in a question to match the utterances we entered, like “What is a bot”:

Hit return and you should see:

Further down in the Inspect Response section, you can hit the detail radio button to see the response object, in this case:

which shows us that it’s hit the correct Intent.

Lambda function

Ok, let’s leave Lex for a few minutes and go make a Lambda function we can use to actually provide an “answer” for that intent.

If you haven’t used Lambdas before, they are an awesome way to run code without any servers i.e. serverless. In AWS, search for Lambda, then look for the Create function button:

There’s again lots of blueprints in here to help you get started, but we’re going to hit the Author from scratch button again, because that’s how we roll:

You’ll see this:

Give it a Name, here I type “myChatbotFunction”.

Leave Role as Choose an existing role (unless you want to create your own one , or use an existing one you may have — entirely up to you) and for Existing role, select lambda_basic_execution, then hit Create function.

After a few seconds, it should be created and you’ll see:

Awesome. You can test it by hitting Test, where you’ll be asked to configure a test event:

I call it myFirstTest, leave the default json input as is, and hit Create. Then back in the main Lambda page, hit Test again and you’ll see a success message; expand out to see:

You’ll note the language is Node.js (6.10):

You can change to Java or Python, but we’ll stick with node. The existing code simply returns a standard hello message:

exports.handler = (event, context, callback) => {
// TODO implement
callback(null, ‘Hello from Lambda’);
};

The event parameter is the input object — what Lex sends the function. This will actually be in this format:

{
"currentIntent": {
"name": "intent-name",
"slots": {
"slot name": "value",
"slot name": "value"
},
"slotDetails": {
"slot name": {
"resolutions" : [
{ "value": "resolved value" },
{ "value": "resolved value" }
],
"originalValue": "original text"
},
"slot name": {
"resolutions" : [
{ "value": "resolved value" },
{ "value": "resolved value" }
],
"originalValue": "original text"
}
},
"confirmationStatus": "None, Confirmed, or Denied (intent confirmation, if configured)",
},
"bot": {
"name": "bot name",
"alias": "bot alias",
"version": "bot version"
},
"userId": "User ID specified in the POST request to Amazon Lex.",
"inputTranscript": "Text used to process the request",
"invocationSource": "FulfillmentCodeHook or DialogCodeHook",
"outputDialogMode": "Text or Voice, based on ContentType request header in runtime API request",
"messageVersion": "1.0",
"sessionAttributes": {
"key": "value",
"key": "value"
},
"requestAttributes": {
"key": "value",
"key": "value"
}
}

Full details on the Lex input / output formats here.

Node.js modules

We can write straight up node in the inline-editor in the browser to parse the input object and send back a response. You could instead create a node package and upload the code as a zip file, by changing the Code entry type to Upload a .ZIP file or Upload a file from S3, but for this basic example, the inline-editor is fine.

Change the code to this:

'use strict';//main handler
exports.handler = (event, context, callback) => {
//log out the input from Lxx
console.log("event: " + JSON.stringify(event));
checkIntent(event, (response) => callback(null, response));
};
//take action based on what intent
function checkIntent(event, callback) {
//get current intent
const name = event.currentIntent.name;
const outputSessionAttributes = event.sessionAttributes || {};
//if ExplainBot intentswitch(name){
case 'ExplainChatbot':
callback(close(outputSessionAttributes, 'Fulfilled', 'A chatbot is an automated way to respond to human queries'));
break;
default:
callback(close(outputSessionAttributes, 'Fulfilled', 'I"m afraid I did not understand the question'));
}
}
function close(sessionAttributes, fulfillmentState, messageContent) {
return {
sessionAttributes,
dialogAction: {
type: 'Close',
fulfillmentState: fulfillmentState,
message: { contentType: 'PlainText', content: messageContent }
},
};
}

and hit Save.

Ok, what’s going on? We changed our main handler to:

//main handler
exports.handler = (event, context, callback) => {
//log out the input from Lxx
console.log("event: " + JSON.stringify(event));
checkIntent(event, (response) => callback(null, response));
};

This logs out the event in a format we can read (to CloudWatch in AWS, accessible via the Monitoring tab), then calls a new function checkIntent:

//take action based on what intent
function checkIntent(event, callback) {
//get current intent
const name = event.currentIntent.name;
const outputSessionAttributes = event.sessionAttributes || {};
//if ExplainBot intentswitch(name){
case 'ExplainChatbot':
callback(close(outputSessionAttributes, 'Fulfilled', 'v'));
break;
default:
callback(close(outputSessionAttributes, 'Fulfilled', 'I"m afraid I did not understand the question'));
}
}

This gets the name of the intent fired by Lex, along with the sessionAttributes object. This can be used to store values we want to persist between interactions, though we won’t be doing that in this tutorial.

We then do a simple check against the name — if it’s ExplainChatbot i.e. matching the Intent we created earlier in Lex, then we call the close function:

function close(sessionAttributes, fulfillmentState, messageContent) {
return {
sessionAttributes,
dialogAction: {
type: 'Close',
fulfillmentState: fulfillmentState,
message: { contentType: 'PlainText', content: messageContent }
},
};
}

which send the “answer” we specified “A chatbot is an automated way to respond to human queries” back to Lex.

Note that we are specifying a type of Close and fulfillmentState as we now consider the “question” answered or “closed”. There are other options — EllicitIntent, EllicitSlot and Delegate that we won’t go into today — more info here.

Testing the Lambda

Before we hook up the Lambda to our Lex bot, we can test the Lambda code by changing the test event — hit Configure test events:

and change the text input code to:

{
"currentIntent": {
"name": "ExplainBot",
"slots": {},
"confirmationStatus": "None"
},
"bot": {
"name": "bot name",
"alias": "bot alias",
"version": "bot version"
},
"userId": "0001",
"inputTranscript": "What is a chatbot",
"invocationSource": "FulfillmentCodeHook",
"outputDialogMode": "text/plain; charset=utf-8",
"messageVersion": "1.0",
"sessionAttributes": {},
"requestAttributes": {}
}

or in its screenshot glory:

This will simulate an input object from Lex. Hit Save and then Test and you should get a successful result:

Tremendous. Ok, back to Lex.

Down the end of the page, change Fulfillment to AWS Lambda function and then in the dropdown select your Lambda function:

This message will pop up:

Just say OK, then hit Save Intent again, then Build once more. Once it’s built, type in “What is a bot” into the test plan and you should get the correct answer:

Boom! This means that our Lex chatbot is now sucessfully linked with our Lambda function. It only has one Intent and gives one answer — but you can build it up from there to have many more.

iOS Integration

Ok, let’s move onto the iOS integration — we want to be able to interact with the Lex bot via a native iOS app.

Before anyone asks what about Android or React Native, we’ll probably cover them off in a later tutorial! Well, Android anyway!

Before we can use the bot in iOS we need to Publish it. Hit that button and you’ll see this:

I gave mine an alias of “first”, then I hit Publish again. You’ll see:

then in a couple of minutes:

Great. There’s an interesting looking section there — How to connect to the your mobile app. Go ahead and hit Download connection info and it’ll take you to this page:

screenshot from https://aws.amazon.com/mobile/sdk/

A better page however is here which goes through in much better detail how to setup the SDK in iOS. We’ll cover it off now ourselves anyway.

Adding LEX SDK into iOS

There are various ways of adding an SDK to an iOS app; we’re going to use Cocoapods.

First create an XCode project, and then in the directory on your Mac which has the project’s .xcodeproj file, run, via the terminal:

gem install cocoapodspod setuppod init

If all went well with those, there’ll now be a new file Podfile in the project directory. Edit and enter this text:

source 'https://github.com/CocoaPods/Specs.git'platform :ios, '0.0'
use_frameworks!
target :'BotBot' do
pod 'AWSCognito'
pod 'AWSLex'
end

Where BotBot is the name of your XCode project. Finally, run:

pod install

You’ll see something like this as it installs:

Once done, close Xcode if its open, and reopen your project by clicking on the .xcworkspace file in the project directory i.e not the .xcodeproj file like you usually would.

AppDelegate.swift changes

The first thing we’ll do in our Xcode project is add the following lines, highlighted in bold, to the AppDelegate.swift file:

import UIKit
import AWSCore
import AWSCognito
import AWSLex
@UIApplicationMain

Then, in the didFinishLaunchingWithOptions function:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {// Override point for customization after application launch.//replace the XXXXXs with your own id
let credentialProvider = AWSCognitoCredentialsProvider(regionType: .USEast1, identityPoolId: "us-east-1:XXXXXXXXXXXXXXXXXXXXXXXX")
let configuration = AWSServiceConfiguration(region: .USEast1, credentialsProvider: credentialProvider)AWSServiceManager.default().defaultServiceConfiguration = configuration//change "botBot" to the name of your Lex bot
let chatConfig = AWSLexInteractionKitConfig.defaultInteractionKitConfig(withBotName: "botBot", botAlias: "$LATEST")
AWSLexInteractionKit.register(with: configuration!, interactionKitConfiguration: chatConfig, forKey: "chatConfig")return true}

The only bit of that code you need to change is the name of the bot from “botBot” to the name of your own, and also the “XXXXXs” of the Cognitio Identity Pool ID to your own. Let’s go get one of those.

Cognito

In AWS go to Cognito:

and then Manage Federated Identities:

Then Create new Identity pool:

Give it an Identity pool name and tick Enable access to unauthenticated identities:

Note — this tutorial doesn’t cover off how to fully secure your app or bot, so what we show may not be secure. Ideally you would not use unauthenticated identities, but it suffices for this example.

When you hit Create Pool you’ll be asked to allow the creation of new roles, hit Allow.

You’ll now be taken to a “Getting started with Amazon Cognito” page. If you change the platform dropdown:

the code samples will change — and you’ll see the same code we used earlier in the AppDelegate.swift, but this time it’ll have the Identity Pool ID that you need:

// Initialize the Amazon Cognito credentials provider  
let credentialsProvider = AWSCognitoCredentialsProvider(regionType:.USEast1, identityPoolId:"us-east-1:XXXXXXXXXXXXXXXXXXXXXX")
let configuration = AWSServiceConfiguration(region:.USEast1, credentialsProvider:credentialsProvider)AWSServiceManager.defaultServiceManager().defaultServiceConfiguration = configuration

Before we go back to the app and use that ID, we need to give the roles created by Cognito, access to Lex.

Go to IAM in AWS, click on Roles, then search for your Cognito role — it’ll be called something like Cognito_LEX_ID_POOLUnauth_… depending on what you called your federated identity.

Click on it, then hit Attach policy, then search for AmazonLexFullAccess:

Select it and hit Attach policy again.

Ok, we should be good to go back to our app. In the AppDelegate.swift file, copy in the identity pool id from AWS, into where I have the XXXXs:

let credentialsProvider = AWSCognitoCredentialsProvider(regionType:.USEast1, identityPoolId:”us-east-1:XXXXXXXXXXXXXXXXXXXXXX”)

ViewController code

Now we’re going to add code to allow the user to enter in a question and see the answer from the bot.

We’re going to do it as simply as possible — we’ll have one View Controller, on which we’ll have one TextField to get the question from the user and a Label to display the answer:

In the ViewController.swift file I have two outlets for the TextField and Label, and at the top of the file I include:

import AWSLex

If it doesn’t recognise it, try cleaning the project.

Now add these delegates to the class signature:

class ViewController: UIViewController, AWSLexInteractionDelegate, UITextFieldDelegate {

along with this variable:

var interactionKit: AWSLexInteractionKit?

This interactionKit is a class from the AWS SDK that we will use to communicate to the Lex Bot.

For the Lex implemenation add a setup function:

func setUpLex(){
self.interactionKit = AWSLexInteractionKit.init(forKey: "chatConfig")
self.interactionKit?.interactionDelegate = self
}

Note that the “chatConfig” matches the key we specified in the AppDelegate.swift earlier:

AWSLexInteractionKit.register(with: configuration!, interactionKitConfiguration: chatConfig, forKey: "chatConfig")

and for the TextField, add this setup function:

func setUpTextField(){
questionTextField.delegate = self
}

These are then both called in the viewDidLoad function:

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.setUpTextField()
setUpLex()

}

For Lex error handling, include this delegate function:

func interactionKit(_ interactionKit: AWSLexInteractionKit, onError error: Error) {
print("interactionKit error: \(error)")
}

and add this function to handle when the user hits the Return key on the keyboard while editing the TextField, to signify they have finished entering their question:

func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
if (questionTextField.text?.characters.count)! > 0 {
sendToLex(text: questionTextField.text!)
}
return true
}

This dismisses the keyboard, checks if the TextField is not empty and then calls the sendToLex function, using the user-entered text in the TextField as a parameter.

Now write that sendToLex function:

func sendToLex(text : String){
self.interactionKit?.text(inTextOut: text, sessionAttributes: nil)
}

That line:

self.interactionKit?.text(inTextOut: text, sessionAttributes: nil)

is where we actually send the question to Lex. It takes two paramaters — a String input aka the question and “sessionAttributes” which we set to nil as we don’t need to send any.

Add this function to handle the response:

//handle responsefunc interactionKit(_ interactionKit: AWSLexInteractionKit, switchModeInput: AWSLexSwitchModeInput, completionSource: AWSTaskCompletionSource<AWSLexSwitchModeResponse>?) {
guard let response = switchModeInput.outputText else {
let response = "No reply from bot"
print("Response: \(response)")
return
}
//show response on screen
DispatchQueue.main.async{
self.answerLabel.text = response
}
}

This basically takes the response from Lex and updates the Label in the ViewContoller.

Ok, that should be everything!! Run the app and you should see:

Go ahead and type “What is a bot?”, hit Enter and you should see:

Yes! It works! Try typing a question it won’t get correct:

and Lex’s standard response for not recognising any intent will be returned.

That’s it! We hope you found this useful, please comment below if you have any questions and feel free to hit the little clap button if you think it worth it! Andy

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Andy O'Sullivan

Andy O'Sullivan

Creator of Boxapopa, the iOS game for young kids with zero ads, just fun! https://apps.apple.com/ie/app/boxapopa/id1550536188