Aws::Lex::Conversation
Building a chatbot with AWS Lex is really fun! Unfortunately implementing your bot's behaviour with the Lex/Lambda event protocol is less fun.
But don't worry - we've done the hard work for you!
Aws::Lex::Conversation
makes it simple to build dynamic, conversational chatbots with AWS Lex and AWS Lambda!
Installation
gem 'aws-lex-conversation'
And then execute:
bundle install
Please Note: This library currently supports AWS Lex Version 2. If you're looking for Lex V1 support, lock aws-lex-conversation
to ~> 3.0
in your Gemfile
.
Core Concepts
The Conversation Instance
Instances of Aws::Lex::Conversation
wrap the Lex input/output event format and make it easy to manage conversation dialog.
Imagine you have a ButlerBot
configured with a ServeBreakfast
intent and BreakfastFood
slot.
The backing lambda handler for your bot might look something like:
require "aws-lex-conversation"
def lambda_handler(event:, context:)
# The conversation instance validates and wraps the Lex input event
conversation = Aws::Lex::Conversation.new(event: event, context: context)
# Return a Close dialog action to our Lex bot and end the conversation
conversation.close(
fulfillment_state: "Fulfilled",
messages: [
# We can construct response messages using wrapper classes.
Aws::Lex::Conversation::Type::Message.new(
content: "Hi - I'm a 🤖!"
),
# Or we can pass a Hash that directly maps to the Lex response format
{
content: "Your intent is: #{conversation.intent_name}",
contentType: "PlainText"
},
{
content: "Here's your #{conversation.slots[:BreakfastFood].value}!",
contentType: "PlainText"
}
]
)
end
This lambda handler would generate the following dialog:
All data from the Lex input event is exposed via the lex
attribute. By convention, the lex
attribute directly translates input event attributes from camelCase
to snake_case
.
We also provide some helpers to help manage the conversation. For example:
# returns true if the lambda function was invoked as a DialogCodeHook
conversation.lex.invocation_source.dialog_code_hook?
# returns true if the InputMode is Speech
conversation.lex.input_mode.speech?
# you can easily set or retrieve a session values
conversation.session[:name] = "Jane"
conversation.session[:name] # => "Jane"
# dealing with slot data is simple
conversation.slots[:BreakfastFood].filled? # returns true if a slot value is present
conversation.slots[:Hometown].blank? # returns true if a slot value is nil/empty
conversation.slots[:FirstName].value # => "John"
The Handler Chain
Conversational dialog gets complex quickly! Conversation instances include a handler chain that can help manage this complexity.
Each handler in the chain defines the prerequisites necessary for the handler to generate a response.
You can configure the handler chain as follows:
def lambda_handler(event:, context:)
conversation = Aws::Lex::Conversation.new(event: event, context: context)
conversation.handlers = [
# You need to define custom handler classes yourself
{ handler: ServeBreakfast },
{ handler: FallbackIntent },
# We offer a few "built in" handlers
{
handler: Aws::Lex::Conversation::Handler::Delegate,
options: {
# If we get this far, always return a Delegate action
respond_on: ->(_) { true }
}
}
]
# The respond method will execute each handler sequentially and return a Lex response
conversation.respond
end
Writing Your Own Handler
Generally, custom behaviour in your flow is achieved by defining your own handler class. Handler classes must:
- Inherit from
Aws::Lex::Conversation::Handler::Base
. - Define a
will_respond?(conversation)
method that returns a boolean value. - Define a
response(conversation)
method to return final response to Lex. This method is called ifwill_respond?
returnstrue
.
Handlers in the chain are invoked sequentially in the order defined.
The first handler in the chain that returns true
for the will_respond?
method will provide the final Lex response action.
Here's an example for the ServeBreakfast
and FallbackIntent
handlers above:
class ServeBreakfast < Aws::Lex::Conversation::Handler::Base
def will_respond?(conversation)
conversation.intent_name == "ServeBreakfast" &&
conversation.slots[:BreakfastFood].filled?
end
def response(conversation)
food = conversation.slots[:BreakfastFood].value
emoji = food == "waffle" ? "🧇" : "🥓"
conversation.close(
fulfillment_state: "Fulfilled",
messages: [
{
content: "Here's your #{emoji}!",
contentType: "PlainText"
}
]
)
end
end
class FallbackIntent < Aws::Lex::Conversation::Handler::Base
def will_respond?(conversation)
conversation.intent_name == "FallbackIntent"
end
def response(conversation)
conversation.close(
fulfillment_state: 'Failed',
messages: [
{
content: "Sorry - I'm not sure what you said!",
contentType: "PlainText"
}
]
)
end
end
Built-In Handlers
Aws::Lex::Conversation::Handler::Echo
This handler simply returns a close response with a message that matches the inputTranscript
property of the input event.
Option | Required | Description | Default Value |
---|---|---|---|
respond_on | No | A callable that provides the condition for will_handle? . |
->(c) { false } |
fulfillment_state | No | The fulfillmentState value (i.e. Fulfilled or Failed ). |
Fulfilled |
content_type | No | The contentType for the message response. |
PlainText |
content | No | The response message content. | conversation.lex.input_transcript |
i.e.
conversation = Aws::Lex::Conversation.new(event: event, context: context)
conversation.handlers = [
{
handler: Aws::Lex::Conversation::Handler::Echo,
options: {
respond_on: ->(c) { true },
fulfillment_state: 'Failed',
content_type: 'SSML',
content: '<speak>Sorry<break> an error occurred.</speak>'
}
}
]
conversation.respond # => { dialogAction: { type: 'Close' } ... }
Aws::Lex::Conversation::Handler::Delegate
This handler returns a Delegate
response to the Lex bot (i.e. "do the next bot action").
Option | Required | Description | Default Value |
---|---|---|---|
respond_on | No | A callable that provides the condition for will_handle? . |
->(c) { false } |
i.e.
conversation = Aws::Lex::Conversation.new(event: event, context: context)
conversation.handlers = [
{
handler: Aws::Lex::Conversation::Handler::Delegate,
options: {
respond_on: ->(c) { true }
}
}
]
conversation.respond # => { dialogAction: { type: 'Delegate' } }
Aws::Lex::Conversation::Handler::SlotResolution
This handler will set all slot values equal to their top resolution in the input event. The handler then calls the next handler in the chain for a response.
NOTE: This handler must not be the final handler in the chain. An exception of type Aws::Lex::Conversation::Exception::MissingHandler
will be raised if there is no successor handler.
Option | Required | Description | Default Value |
---|---|---|---|
respond_on | No | A callable that provides the condition for will_handle? . |
->(c) { true } |
i.e.
conversation = Aws::Lex::Conversation.new(event: event, context: context)
conversation.handlers = [
{
handler: Aws::Lex::Conversation::Handler::SlotResolution
},
{
handler: Aws::Lex::Conversation::Handler::Delegate,
options: {
respond_on: ->(c) { true }
}
}
]
conversation.respond # => { dialogAction: { type: 'Delegate' } }
Advanced Concepts
This library provides a few constructs to help manage complex interactions:
Data Stash
Aws::Lex::Conversation
instances implement a stash
method that can be used to store temporary data within a single invocation.
A conversation's stashed data will not be persisted between multiple invocations of your lambda function.
The conversation stash is a great spot to store deserialized data from the session, or invocation-specific state that needs to be shared between handler classes.
This example illustrates how the stash can be used to store deserialized data from the session:
# given we have JSON-serialized data in as a persisted session value
conversation.session[:user_data] = '{"name":"Jane","id":1234,"email":"test@example.com"}'
# we can deserialize the data into a Hash that we store in the conversation stash
conversation.stash[:user] = JSON.parse(conversation.session[:user_data])
# later on we can reference our stashed data (within the same invocation)
conversation.stash[:user] # => {"name"=>"Jane", "id"=>1234, "email"=>"test@example.com"}
Checkpoints
A conversation may transition between many different topics as the interaction progresses. This type of state transition can be easily handled with checkpoints.
When a checkpoint is created, all intent and slot data is encoded and stored into a checkpoints
session value. This data persists between invocations, and is not removed until the checkpoint is restored.
You can create a checkpoint as follows:
# we're ready to fulfill the OrderFlowers intent, but we want to elicit another intent first
conversation.checkpoint!(
label: 'order_flowers',
dialog_action_type: 'Close' # defaults to 'Delegate' if not specified
)
conversation.elicit_intent(
messages: [
{
content: 'Thanks! Before I place your order, is there anything else I can help with?',
contentType: 'PlainText'
}
]
)
You can restore the checkpoint in one of two ways:
# in a future invocation, we can fetch an instance of the checkpoint and easily
# restore the conversation to the previous state
checkpoint = conversation.checkpoint(label: 'order_flowers')
checkpoint.restore!(
fulfillment_state: 'Fulfilled',
messages: [
{
content: 'Okay, your flowers have been ordered! Thanks!',
contentType: 'PlainText'
}
]
) # => our response object to Lex is returned
It's also possible to restore state from a checkpoint and utilize the conversation's handler chain:
class AnotherIntent < Aws::Lex::Conversation::Handler::Base
def will_respond?(conversation)
conversation.intent_name == 'AnotherIntent' &&
conversation.checkpoint?(label: 'order_flowers')
end
def response(conversation)
checkpoint = conversation.checkpoint(label: 'order_flowers')
# replace the conversation's current resolved intent/slot data with the saved checkpoint data
conversation.restore_from!(checkpoint)
# call the next handler in the chain to produce a response
successor.handle(conversation)
end
end
class OrderFlowers < Aws::Lex::Conversation::Handler::Base
def will_respond?(conversation)
conversation.intent_name == 'OrderFlowers'
end
def response(conversation)
conversation.close(
fulfillment_state: 'Fulfilled',
messages: [
{
content: 'Okay, your flowers have been ordered! Thanks!',
contentType: 'PlainText'
}
]
)
end
end
conversation = Aws::Lex::Conversation.new(event: event, context: context)
conversation.handlers = [
{ handler: AnotherIntent },
{ handler: OrderFlowers }
]
conversation.respond # => returns a Lex response object
Test Helpers
This library provides convenience methods to make testing easy! You can use the test helpers as follows:
# we must explicitly require the test helpers
require 'aws/lex/conversation/spec'
# optional: include the custom matchers if you're using RSpec
RSpec.configure do |config|
config.include(Aws::Lex::Conversation::Spec)
end
# we can now simulate state in a test somewhere
it 'simulates a conversation' do
conversation # given we have an instance of Aws::Lex::Conversation
.simulate! # simulation modifies the underlying instance
.transcript('My age is 21') # optionally set an input transcript
.intent(name: 'MyCoolIntent') # route to the intent named "MyCoolIntent"
.slot(name: 'Age', value: '21') # add a slot named "Age" with a corresponding value
expect(conversation).to have_transcript('My age is 21')
expect(conversation).to route_to_intent('MyCoolIntent')
expect(conversation).to have_slot(name: 'Age', value: '21')
end
# if you'd rather create your own event from scratch
it 'creates an event' do
simulator = Aws::Lex::Conversation::Simulator.new
simulator
.transcript('I am 21 years old.')
.input_mode('Speech')
.context(name: 'WelcomeGreetingCompleted')
.invocation_source('FulfillmentCodeHook')
.session(username: 'jane.doe')
.intent(
name: 'GuessZodiacSign',
state: 'ReadyForFulfillment',
slots: {
age: {
value: '21'
}
}
)
event = simulator.event
expect(event).to have_transcript('I am 21 years old.')
expect(event).to have_input_mode('Speech')
expect(event).to have_active_context(name: 'WelcomeGreetingCompleted')
expect(event).to have_invocation_source('FulfillmentCodeHook')
expect(event).to route_to_intent('GuessZodiacSign')
expect(event).to have_slot(name: 'age', value: '21')
expect(event).to include_session_values(username: 'jane.doe')
end
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/amaabca/aws-lex-conversation. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the Aws::Lex::Conversation
project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.