Building a Slackbot

NodeJS + Lambda + Lex

twitter.com/@johncmckim

johncmckim.me

medium.com/@johncmckim

Software Development Lead

A Cloud Guru

John McKim

@johncmckim

What is A Cloud Guru

Training Courses for Engineers

https://acloud.guru

AWS Chatbot Callenge

Create conversational, intelligent chatbots using Amazon Lex and AWS Lambda

AWS Chatbot Callenge

Building a Chatbot in a weekend

AWS Chatbot Callenge

Building a Chatbot in a weekend

Agenda

  • What is Slack
  • What did I build
  • Developing a chatbot for Slack
  • Adding intelligence with Lex
  • What I learnt

What is Slack

Where work happens (supposedly)

What did I build

A Cloud Guru Quiz Bot

What did I build

A Cloud Guru Quiz Bot

What did I build

But how?

  • AWS Lambda
  • AWS Lex
  • Slack install API
  • Slack Events API
  • Slack Interactive Messages API

Stop. Demo Time

A Cloud Guru Quiz bot

Before the fun stuff

Slack Configuration

Creating an App

https://api.slack.com/apps

Creating an App

https://api.slack.com/apps

Creating an App

https://api.slack.com/apps

Creating an App

https://api.slack.com/apps

Creating an App

https://api.slack.com/apps

Developing a Slackbot

What is AWS Lambda

Serverless Compute Service on AWS

  • Functions as unit of Deployment
  • Functions as unit of Scale
  • Scaling managed by AWS
  • Never pay for idle
  • Event driven

Developing a Slackbot

What is AWS Lex

The thing that powers Alexa

  • AI as a Service
  • Natural Language Understanding
  • Define Intents
  • Use an API to determine intent from Text

Developing a Slackbot

The Architecture

Developing a Slackbot

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:
          ...

Developing a Slackbot

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
}

Developing a Slackbot

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 });
};

Adding Intelligence with Lex

Understanding Humans

Adding Intelligence with Lex

Understanding the Intent

Adding Intelligence with Lex

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,
};

Adding Intelligence with Lex

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 Intelligence with Lex

Adding some fun

What I Learnt

Beware of duplicate Events

  • Slack will send retries
    • Return message to Slack ASAP
    • Coldstarts increase likelihood of duplicate messages
  • You can use Lex without using their platform integrations

What I Learnt

Future Architecture

Thanks for Listening!

Questions?

twitter.com/@johncmckim

johncmckim.me

medium.com/@johncmckim

Building a Slackbot with Lex

By John McKim

Building a Slackbot with Lex

  • 254