twitter.com/@johncmckim
johncmckim.me
medium.com/@johncmckim
Software Development Lead
A Cloud Guru
John McKim
@johncmckim
Training Courses for Engineers
https://acloud.guru
Create conversational, intelligent chatbots using Amazon Lex and AWS Lambda
Building a Chatbot in a weekend
Building a Chatbot in a weekend
Where work happens (supposedly)
A Cloud Guru Quiz Bot
A Cloud Guru Quiz Bot
But how?
A Cloud Guru Quiz bot
Slack Configuration
https://api.slack.com/apps
https://api.slack.com/apps
https://api.slack.com/apps
https://api.slack.com/apps
https://api.slack.com/apps
What is AWS Lambda
Serverless Compute Service on AWS
What is AWS Lex
The thing that powers Alexa
The Architecture
Serverless Framework Config
service: studybots-service
frameworkVersion: ">=1.11.0 <2.0.0"
provider:
name: aws
runtime: nodejs6.10
iamRoleStatements:
... (Allow dynamodb & lex)
functions:
slackInstall:
handler: src/slack/install/handler.handler
environment:
... configure env vars
events:
- http:
path: slack/v1/install
method: get
slackEvents:
handler: src/slack/events/handler.handler
environment:
... configure env vars
events:
- http:
path: slack/v1/events
method: post
slackActions:
handler: src/slack/actions/handler.handler
environment:
... configure env vars
events:
- http:
path: slack/v1/actions
method: post
quizPointsCalculator:
handler: src/quiz/points/quiz-stream-handler.handler
environment:
... configure env vars
events:
- stream:
type: dynamodb
arn:
Fn::GetAtt:
- QuizzesTable
- StreamArn
resources:
Resources:
SlackTeamsTable:
Type: AWS::DynamoDB::Table
DeletionPolicy: ${self:custom.config.dynamodb.deletion_policy}
Properties:
TableName: ${self:custom.dynamodb.slack_teams.table_name}
AttributeDefinitions:
- AttributeName: team_id
AttributeType: S
KeySchema:
- AttributeName: team_id
KeyType: HASH
ProvisionedThroughput:
...
QuizzesTable:
Type: AWS::DynamoDB::Table
DeletionPolicy: ${self:custom.config.dynamodb.deletion_policy}
Properties:
TableName: ${self:custom.dynamodb.quizzes.table_name}
AttributeDefinitions:
- AttributeName: quiz_id
AttributeType: S
KeySchema:
- AttributeName: quiz_id
KeyType: HASH
ProvisionedThroughput:
...
StreamSpecification:
...
TimeToLiveSpecification:
...
QuizPointsTable:
Type: AWS::DynamoDB::Table
DeletionPolicy: ${self:custom.config.dynamodb.deletion_policy}
Properties:
TableName: ${self:custom.dynamodb.quiz_points.table_name}
AttributeDefinitions:
- AttributeName: group_id
AttributeType: S
- AttributeName: board_key
AttributeType: S
- AttributeName: user_id
AttributeType: S
- AttributeName: period
AttributeType: S
KeySchema:
- AttributeName: group_id
KeyType: HASH
- AttributeName: board_key
KeyType: RANGE
ProvisionedThroughput:
...
GlobalSecondaryIndexes:
- IndexName: user-index
KeySchema:
- AttributeName: group_id
KeyType: HASH
- AttributeName: user_id
KeyType: RANGE
Projection:
ProjectionType: ALL
ProvisionedThroughput:
...
- IndexName: period-index
KeySchema:
- AttributeName: group_id
KeyType: HASH
- AttributeName: period
KeyType: RANGE
Projection:
ProjectionType: ALL
ProvisionedThroughput:
...
TimeToLiveSpecification:
...
Handling a Slack event
{
"token": "xxxxx",
"team_id":"TXXX",
"api_app_id":"AXXXX",
"event": {
"type":"message",
"user":"UXXX",
"text":"some text",
"ts":"1500637786.235683",
"channel":"CXXXX",
"event_ts":"1500637786.235683"
},
"type":"event_callback",
"authed_users":["UXXXX"],
"event_id":"EXXXXX",
"event_time":1500637786
}
Handling a Slack event
'use strict';
const service = require('./service');
module.exports.handler = (event, context, cb) => {
console.log('Received event', event);
const request = JSON.parse(event.body);
if (request.type === 'url_verification') {
// Handle url verification
return;
}
const slackEvent = request.event;
if (!slackEvent) {
console.log('Missing slack event');
cb(null, { statusCode: 500 });
return;
}
if (slackEvent.type === 'message' && slackEvent.subtype !== 'bot_message' && slackEvent.subtype !== 'message_changed') {
return service
.startOnMessage(request)
.then((result) => {
console.log('Returning result');
cb(null, { statusCode: 200 });
return result.continue ? service.continueOnMessage(result.team, request) : Promise.resolve();
})
.catch((err) => {
console.log('Error', err);
cb(err);
});
}
console.log('Unhandled event type');
cb(null, { statusCode: 200 });
};
Understanding Humans
Understanding the Intent
Using Lex
'use strict';
const AWS = require('aws-sdk');
const lexruntime = new AWS.LexRuntime();
const getMessageIntent = (team, event) => {
const regex = new RegExp(`<@${team.bot.bot_user_id}>`);
const text = event.text.replace(regex, '')
const params = {
botAlias: process.env.LEX_BOT_ALIAS,
botName: process.env.LEX_BOT_NAME,
inputText: text,
userId: event.user,
};
console.log('Detecting intent', params);
return new Promise((resolve, reject) => {
lexruntime.postText(params, (err, data) => {
console.log('Received postText result', err, data);
if (err) {
reject(err); // an error occurred
} else {
resolve({
name: data.intentName,
slots: data.slots
});
}
});
});
};
module.exports = {
getMessageIntent,
};
Handling Intents
const continueOnMessage = (team, message) => {
const event = message.event;
const channel = event.channel;
const user = event.user;
return intentsService
.getMessageIntent(team, event)
.then((intent) => {
console.log('Detected intent: ', intent);
const slots = intent.slots || {};
switch(intent.name) {
case 'AreYouBroken':
return helpIntents.areYouBroken(team, channel, user, slots);
case 'Help':
return helpIntents.help(team, channel, user, slots);
case 'HiThere':
return helpIntents.hiThere(team, channel, user, slots);
case 'StarWarsHelpMe':
return funIntents.learnTheWaysOfTheCloud(team, channel, user, slots);
case 'StartQuiz':
return quizService.startQuiz(team, channel, user, slots);
case 'Leaderboard':
return leaderboardIntents.getLeaderboard(team, channel, user, slots);
default:
console.log('Intent not implemented', intent);
return helpIntents.missIntent(team, channel, user);
}
});
};
Adding some fun
Beware of duplicate Events
Future Architecture
twitter.com/@johncmckim
johncmckim.me
medium.com/@johncmckim