Using Amazon Lex V2 with Kotlin Amazon Lambda handler

Benjamin Peter
SnapSoft.io Blog
Published in
8 min readFeb 13, 2023

Using Amazon Lex you can build various chatbots, but out of the box, it is not capable of complex actions like integration with other third-party tools, for example, Google Calendar. In order to handle more complex actions we have to use Amazon Lambda.

In this article, we will go through some examples using Amazon Lambda for input validation and fulfillment while using Kotlin as our programming language.

This article will touch on the following topics:

  • Setting up a simple Amazon Lex V2 bot
  • Setting up a simple Amazon Lambda function using Kotlin
  • Processing Lex input, fulfillment response
  • Validating Lex input, elicit response

Setting up a simple Amazon Lex V2 bot

On the Amazon Lex console click the create bot button. Once the creation page is open select Create a blank bot, choose a name and for the simplicity of the example select Create a role with basic Amazon Lex permissions. Click next, on the next screen you can set up multiple languages, for now, continue with just an English one. Once done with this page click create. Now you can see the intent screen. An intent basically represents an action that the user wants to perform. It’s possible to have multiple intents for a bot. In order for our bot to understand we want to use this intent we have to set up sample utterances. Let’s add a simple one: Book me a flight. As you can see from this it’s hard to decide from where the flight should origin and where the destination shall be, for this we can use slots. Slots are like parameters in a sentence. In these, we can store data and later on process them. Let’s add the following slots:

  • origin — the origin of the flight — use Slot type AMAZON.City
  • destination — the destination of the flight — use Slot type AMAZON.City
  • date — the date of the flight — use Slot type AMAZON.Date

We can also provide a prompt message in case the user doesn’t provide a required slot or, the provided slot is invalid. For example if for the date we provide a random text the bot will ask the user again for the date.

If all the slots are added let’s extend the sample utterances with the following:

  • Book me a flight from {origin} to {destination} on {date}
  • Book me a flight on {date}
  • Book me a flight to {destination} on {date}

As you can see you have to provide the slots in curly brackets to use them. Be careful with adding too many utterances. That could make the bot over-learn and won’t be able to understand more generic prompts.

Once we have configured our utterances in order to get a simple bot running it’s useful to have a response so we know that everything we want ran. Under the fulfillment section provide an On successful fulfillment response. When you are done click Build on the top right of the intent window, once build click Test and you can test the bot.

Example conversation

Setting up a simple Amazon Lambda function using Kotlin

In order to handle more complex actions we will need an Amazon Lambda function. On the Amazon Lambda console page click Create function. On the creation page select author from scratch option, provide a name for the function, and select Java 11 (Corretto) as the runtime since we will be using Kotlin. Once everything is configured click Create function . As for the code that we want to upload to Lambda, for the dependencies I’ll be using maven however you can use a different dependency manager and build tool. The project shall be using Java 11 (Corretto).

In order to handle Amazon Lambda requests the following dependency will be required:

<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-core</artifactId>
</dependency>

And in order to build our dependencies into the jar file add the following plugin:

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.4.2</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>

To handle our requests we shall provide a handler. Create a file that will contain the handler code. In my case, this will be Handler.kt just for testing purposes print something to the default console and return the event. The aws-lambda-java-core library provides us a RequestHandler<I,O> interface with which we can provide the input type and the output type. It’ll also provide us with a function called handleRequest which we have to override and there we can handle the requests. Since currently we don’t know the type of our input and output let’s set them to Any and add the following code:

import com.amazonaws.services.lambda.runtime.Context
import com.amazonaws.services.lambda.runtime.RequestHandler

class Handler: RequestHandler<Any, Any> {

override fun handleRequest(event: Any, context: Context): Any {
println("Kotlin handler.")
return event
}
}

Now if we run the mvn package the previously added plugin will generate a jar file in our target folder called {name}-jar-with-dependencies.jar. We can upload this file on the Lambda console using the Upload from dropdown and selecting .zip or .jar file . In order to call the function we have to set up the bot to use it for fulfillment. Navigate to the intent settings and under the fulfillment tab select Advanced options and tick the Use a Lambda function for fulfillment box.

Fulfillment code hook

Rebuild the bot. Once done go back to the settings page and under Aliases-> BotAliases select the language and select the created Lambda function as the source and save it. Now if the fulfillment step happens while talking with the bot it shall throw an exception but if we check the Lambdas CloudWatch logs we will see that our handler did run successfully:

CloudWatch log of a successful conversation

Processing Lex input, fulfillment response

Currently, our bot is broken. The reason is that we are not really sending back the right response to Lex. In order to fix the error we shall add a few more dependencies:

<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>lexruntimev2</artifactId>
<version>2.19.26</version>
</dependency>

<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>

The first dependency will be used for the LexV2 models so we won’t have to map everything. The second one will be used in order to parse the input and output.

First off let’s check out what properties we will be using:

  • sessionState: stores our current session like our current Intent, the evaluated slots
  • sessionState.sessionAttributes: stores attributes used in our current session, this could be used for authorization tokens and such
  • sessionState.dialogAction: stores the actions which the Lex bot shall proceed with
  • sessionState.dialogAction.type: type of the dialog action this can be the following
  • sessionState.dialogAction.slotToElicit: is an optional field, we have to provide this if the type is `ElicitSlot`
  • sessionState.intent: current intent which is being used. This stores the current slots and their evaluated values
  • sessionState.intent.slots: contains all the slots evaluated by Lex
  • sessionState.intent.confirmationState: contains the confirmation state of the intent
  • sessionState.intent.name: name of the current intent
  • sessionState.intent.state: state of the current intent

In a response we have to provide two properties in our response:

  • sessionState: the original sessionState but modified based on our needs, for example with a modified dialog action
  • messages: list of messages that the bot shall show the user.

In order to create a close response we have to set the following data:

  • sessionState.dialogAction: This has to be set to Close
  • sessionState.intent.confirmationState: This shall be set to Confirmed
  • sessionState.intent.state: This shall be set to Fulfilled in case the intent was fulfilled or Failed if the intent failed.
  • messages: This array should contain all the messages. In our case, this will be a simple PlainText message but it could be a CardResponse or a CustomPayload too.

Now that we understand the input and the output we are prepared to handle a fulfillment request and return a valid response.

First off let’s create our input type:

import software.amazon.awssdk.services.lexruntimev2.model.SessionState

data class LexV2Event(
var sessionState: SessionState,
)

As you can see from above the input will only contain a sessionState (at least currently that’s all we are interested in). At this point we have two options for handling the requests:

  • Use the RequestHandler<I,O> interface which we have already added. In this case, we have to update our LexV2Event data class since when parsing the input it will need a default constructor, and this way we can’t provide one.
  • Use the RequestStreamHandler interface with which we can handle the requests as streams. I’ll be working with this approach.

So first off let’s change the interface on our handler class and update the handler implementation.

import com.amazonaws.services.lambda.runtime.RequestStreamHandler

class Handler : RequestStreamHandler {
override fun handleRequest(inputStream: InputStream, outputStream: OutputStream, context: Context?) {
//...
}
}

Add a few static functions which will help us parse the input stream into our data class and parse our object for our outputStream:

private inline fun <reified T> Gson.fromJson(inputStream: InputStream): T =
InputStreamReader(inputStream).use { fromJson(it, T::class.java) }

private fun Gson.toJson(value: Any, outputStream: OutputStream): Unit =
OutputStreamWriter(outputStream).use { toJson(value, it) }

Next, add a function that will handle creating our close responses:

private fun closeResponse(intentState: IntentState, event: LexV2Event) = Response(
sessionState = event.sessionState.toBuilder()
.dialogAction(DialogAction.builder().type(DialogActionType.CLOSE).build())
.intent(
event.sessionState.intent().toBuilder()
.confirmationState(ConfirmationState.CONFIRMED)
.state(intentState).build()
)
.build(),
messages = listOf(
Message.builder().content("This is a message from the kotlin handler.")
.contentType(MessageContentType.PLAIN_TEXT).build()
),
)

This function uses the original event to make builders from their properties and rebuild them with the modified values. Now we can extend our handler function to return a close response:

override fun handleRequest(event: LexV2Event, context: Context): Response {
return closeResponse(IntentState.FULFILLED, event)
}

Now if we build our project and upload it to the Lambda console we won’t have an error and we will have a successful conversation with our bot:

Successful fulfillment

Summary

Amazon Lex can be a powerful tool if we use it the right way. On the other hand, it is really simple to overcomplicate easy tasks.

When using Amazon Lex be careful how many utterances you provide and what language you use for handlers. You will see that cold starts are quite slow when using languages like Kotlin/Java or .Net on the other hand Node.js is really fast.

In this article, we went through the basics of Amazon Lex and created a simple bot that has a handler using Amazon Lambda which is written in Kotlin. The code is available on GitHub.

If you are interested in how we at SnapSoft use cloud solutions to build modern applications with top technologies such as AWS, Terraform, and AI/ML services, get in touch — we’d love to talk to you.

About the author

Benjamin Peter is a Software Engineer at SnapSoft Ltd. He has been working as a Web Developer and started working with Amazon Services. Lately, he has started looking into Amazon Machine Learning Services like Amazon Lex and Amazon Rekognition. Connect with Benjamin on LinkedIn.

--

--

Benjamin Peter
SnapSoft.io Blog

I've been working as a Software Engineer since 2019. I've mostly worked with web applications but in the past I had the change to look into AWS Services.