In this post I explore the basics on creating a Lex Bot as code. AWS Lex is a service that allows you to create a bot to respond to certain chat scripts. You essentially define the various ways people will talk to the bot, and then define AWS Lambda functions to actually perform the action requested and respond with the result of that action, or possibly continue on in the script to gather more information or merge into a new conversation altogether.
This isn’t my first soiree into Chat Bots. The astute observer will remember the post IBM BlueMix Cognitive Services – Conversation API and the Ships Computer where we went through the process of creating a chatbot using IBM Bluemix that mimic’d the functions of the USS Enterprise Computer. While that was a silly endeavor, this one is more fundamental.
The Problem with Chat Bot Versioning
AWS Lex and AWS Lambda, unfortunately, have a versioning system that is linear in nature. With Lambda, it sort of makes sense. Lambda’s are small units of script that usually perform very few functions. One developer can work on them and manage the simple versioning without much thought. If each lambda is a contained development process – with small stable team working on it – a linear versioning model with the alias tagging system works okay.
But the problem comes in with the Chat Bot. Some chat bots are quite complex. They require modifications, updates, as they experience more and more inputs. They are trained by a manual review process – a buggy interface that reports all the “utterances” that it did not recognize. This interface is updated once per 24 hours, but even that is buggy.
The interface as a whole to manage the chat bot, has a lot of versioning on a lot of different pieces. As a result – you might find yourself unsure what version exactly you are modifying of each part of the bot. All edits in the interface are done to a version dubbed $LATEST. This means – definitively – the bot is a one person at a time interface. If two people go to work on the lex bot simultaneously, they are unknowingly overwriting each other, possibly on the same intent or slot. If one builds their version, the $LATEST could have been updated since they last refreshed the interface. As a result, what they end up testing fails, and huge delays are induced in development.
Now, to compound this, the lambda’s that are used to fullfill the Lex bot intents, are independently versioned. They have a $LATEST too, which of course suffers the same clobbering problem. However, once you go to setting alias’s to lambda versions – you cannot select between these in the Lex interface. The whole mechanism to set these things up with more than a single developer is cumbersome.
Lex Bot as Code – Separating model from Deployment
Lex, like Lambda, is essentially a configuration that is built/packaged and served at a versioned endpoint. Alias’s can be assigned to each of those versions. All of the configuration and the deployment is part of the Lex interface. Its not clear from the interface, a good way to parallelize development. One developer can clobber another without even knowing.
For example, lets suppose we want to write a bot that is comprised of over 40 intents – each having 40 lambda fulfillment functions. You might enlist the help of 10 developers working in parallel to build this bot out quickly. Unfortunately, 10 folks working in this singular interface will not work. The state is constantly being overwritten and there is no medium for a developer to seperate the state long enough for them to complete and commit a feature without another erasing their work.
So we pull the model out of the Lex interface. Lex, at its core, is made of Slots, Intents, Bots, and Lambdas. So we specify the Slots, Intents, and Bots as YAML definitions, and the lambdas as a YAML and associated script file. Using those YAML definitions (which collectively represent an unclobberable state a developer can reliably develop on) the developer can edit the state, and then automatically commit that state to version, assign alias to it, and then automate test cases on the bot. These things all happen in concert and partition off development to their individual alias – such that they can work insulated from others.
Since the YAML and scripts can be checked into a version control system, we can now use git branches tracked to various alias. This allows features to be worked on and tested and eventually merged into the master branch, which will then update the PROD alias on the bot. Without scaffolding to manage this parallel development, it would be impossible to develop large chatbots without significant effort in development effort synchronization.
HelloWorldBot – a walkthrough of the LBaC repo
(Note – not all of the above is currently implemented in the LBaC repo – it is a work in progress. If you want to contribute – please do!)
The LBaC HelloWorldBot source code is available here. You will need Python2.7 and boto3
Structure
The main parts of the LBaC repo are
- deploy.py – this is a python script that will deploy the LB model to the $LATEST version of Lex and Lambda
- slots/*.yaml – These are the model definition files for Slot Types in Lex. In the HelloWorld boilerplate we define a single slot type – Name
1234567891011121314name: "Name"description: "A name to be included in an utterance"enumerationValues:- value: "Todd"- value: "Robert"- value: "Allen"- value: "Karen"- value: "Stephanie"- value: "Linda"- value: "Becky"- value: "Terry"- value: "Tom"- value: "Dick"- value: "Harry" - intents/*.yaml – These are the model definition files for the Intents in Lex. In the HelloWorld boilerplate we define a single intent – HelloWorld
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152name: "HelloWorld"description: "Bot says hello"slots:- name: "Name"description: "The supplied name"slotConstraint: "Required"slotType: "Name"slotTypeVersion: "$LATEST"valueElicitationPrompt:messages:- contentType: "PlainText" #SSMLcontent: "With whom do I have the pleasure of speaking?"maxAttempts: 2sampleUtterances:- "My name is {Name}"- "Im {Name}"sampleUtterances:- "hello"- "hey"- "Whats up"- "How are you"#confirmationPrompt:# messages:# - contentType: "PlainText"# content: "Say hello?"# maxAttempts: 2#rejectionStatement:# messages:# - contentType: "PlainText"# content: "Okay I wont say hello"#followUpPrompt:# prompt:# messages:# - contentType: "PlainText"# content: "Sorry this isnt working"# maxAttempts: 2# rejectionStatement:# messages:# - contentType: "PlainText"# content: "Sorry this isnt working"#conclusionStatement:# messages:# - contentType: "PlainText"# content: "Sorry this isnt working"#dialogCodeHook:# uri: 'arn:aws:lambda:us-west-2:account-id:function:hello:DEV'# messageVersion:fulfillmentActivity:type: 'CodeHook'codeHook:uri: 'arn:aws:lambda:us-east-1:878047780435:function:SayHello'messageVersion: "1.0"
- bots/*.yaml – These are the model definition files for the Bots in Lex. In the Hello World boilerplate we define a single bot – HelloWorldBot
12345678910111213141516171819name: "HelloWorldBot"description: "A Hello World Bot"intents:- intentName: "HelloWorld"intentVersion: "$LATEST"# - intentName: "goodbye"# intentVersion: 1clarificationPrompt:messages:- contentType: 'PlainText'content: "I do not understand, please reword your query."maxAttempts: 2abortStatement:messages:- contentType: 'PlainText'content: "I do not understand, please reword your query."idleSessionTTLInSeconds: 123locale: 'en-US'childDirected: False
- lambda/*.yaml – These are the model definition files for the Lambdas in Lex. In the HelloWorld boilerplate we definte a single lambda – SayHello
1234567891011121314151617181920212223242526272829FunctionName: "SayHello"Runtime: "python2.7" #'nodejs'|'nodejs4.3'|'nodejs6.10'|'java8'|'python2.7'|'python3.6'|'dotnetcore1.0'|'nodejs4.3-edge'Role: "arn:aws:iam::878047780435:role/service-role/LexLambda"Handler: "sayhello.execute"Code:file: "sayhello.py"# 'ZipFile': b'bytes'# 'S3Bucket': 'string'# 'S3Key': 'string'# 'S3ObjectVersion': 'string'Description: "A lambda function to say hello to the provided name"Timeout: 3MemorySize: 128Publish: True#VpcConfig:# SubnetIds:# - "value"# SecurityGroupIds:# - "value"#DeadLetterConfig:# TargetArn:#Environment:# Variables:# ET: "PhoneHome"#KMSKeyArn: "arn:aws:iam::878047780435:"#TracingConfig:# Mode: "Active" #"PassThrough"#Tags:# ET: "PhoneHome"
- lambda/code/* – these are the scripts that actually make up the Lambda. In the HelloWorld boilerplate we define a script for the SayHello lambda defined in the above directory
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145"""This sample demonstrates an implementation of the Lex Code Hook Interfacein order to serve a sample bot which manages reservations for hotel rooms and car rentals.Bot, Intent, and Slot models which are compatible with this sample can be found in the Lex Consoleas part of the 'BookTrip' template.For instructions on how to set up and test this bot, as well as additional samples,visit the Lex Getting Started documentation http://docs.aws.amazon.com/lex/latest/dg/getting-started.html."""import timeimport osimport logginglogger = logging.getLogger()logger.setLevel(logging.DEBUG)# --- Helpers that build all of the responses ---def elicit_slot(session_attributes, intent_name, slots, slot_to_elicit, message):return {'sessionAttributes': session_attributes,'dialogAction': {'type': 'ElicitSlot','intentName': intent_name,'slots': slots,'slotToElicit': slot_to_elicit,'message': message}}def confirm_intent(session_attributes, intent_name, slots, message):return {'sessionAttributes': session_attributes,'dialogAction': {'type': 'ConfirmIntent','intentName': intent_name,'slots': slots,'message': message}}def close(session_attributes, fulfillment_state, message):response = {'sessionAttributes': session_attributes,'dialogAction': {'type': 'Close','fulfillmentState': fulfillment_state,'message': message}}return responsedef delegate(session_attributes, slots):return {'sessionAttributes': session_attributes,'dialogAction': {'type': 'Delegate','slots': slots}}# --- Helper Functions ---def safe_int(n):"""Safely convert n value to int."""if n is not None:return int(n)return ndef try_ex(func):"""Call passed in function in try block. If KeyError is encountered return None.This function is intended to be used to safely access dictionary.Note that this function would have negative impact on performance."""try:return func()except KeyError:return Nonedef say_hello(intent_request):"""Performs dialog management and fulfillment for booking a car.Beyond fulfillment, the implementation for this intent demonstrates the following:1) Use of elicitSlot in slot validation and re-prompting2) Use of sessionAttributes to pass information that can be used to guide conversation"""slots = intent_request['currentIntent']['slots']hiname = slots['Name']# confirmation_status = intent_request['currentIntent']['confirmationStatus']##session_attributes = intent_request['sessionAttributes'] if intent_request['sessionAttributes'] is not None else {}return close({},'Fulfilled',{'contentType': 'PlainText','content': "I am a Lex Chatbot. Hello %s" %hiname})# --- Intents ---def dispatch(intent_request):"""Called when the user specifies an intent for this bot."""logger.debug('dispatch userId={}, intentName={}'.format(intent_request['userId'], intent_request['currentIntent']['name']))intent_name = intent_request['currentIntent']['name']if intent_name == 'HelloWorld':return say_hello(intent_request)raise Exception('Intent with name ' + intent_name + ' not supported')# --- Main handler ---def execute(event, context):"""Route the incoming request based on intent.The JSON body of the request is provided in the event slot."""# By default, treat the user request as coming from the America/New_York time zone.os.environ['TZ'] = 'America/Denver'time.tzset()logger.debug('event.bot.name={}'.format(event['bot']['name']))return dispatch(event)
The model files are all parameter matches from the corresponding boto3 methods. If you are unsure what a parameter means – or the values it can take – refer to the LexModelBuildingService documentation for boto3
Deploying the model
Once the model is correctly set, the developer simply runs the deploy.py script. Remember to set your AWS credentials in your ~/.aws directory so that boto can actually perform the actions. The deploy script translates your model – to become the active $LATEST version of the bot and lambdas on AWS.
From there you can immediately build and version the bot.
Next Steps
In the next steps we are looking to make a release and development pipeline around the deployments – automatically building, publishing, assigning alias, and running automated test cases around the robotos and their various channels.