Building Alexa Skills

NodeJS + Lambda

twitter.com/@johncmckim

johncmckim.me

medium.com/@johncmckim

Software Engineer at A Cloud Guru

John McKim

@johncmckim

Contribute to Serverless Framework

https://acloud.guru

What is A Cloud Guru

Training Courses for Engineers

Serverless Framework

https://serverless.com

Agenda

  • What is Alexa
  • Alexa re:Invent Competition
  • Developer Registration
  • Configuring an Alexa Skill
  • Developing a Lambda for Alexa
  • Bonus Level - GraphQL & React
  • What I learnt

What is Alexa

Amazon Echo and Echo Dot

What is Alexa

What can it do

  • Order from amazon.com
  • Play music - Spotify / Amazon music
  • Read books
  • Alarm Clock
  • Control a Smart Home
  • Run custom Skills (like apps)

What is Alexa

What can I make it do?

  • Voice Activated Pitching Machine

  • Baby monitor

  • Control Servers - (cause chaos)

  • Bris JS reminder

  • Anything ?

Alexa re:Invent Contest

Alexa re:Invent Contest

Close but no Cigar

No Echo. No Problems.

https://echosim.io

Before the fun stuff

Registration & Configuration

Developer Registration

https://developer.amazon.com

Create Account

Developer Registration

https://developer.amazon.com

Hand over your details

Developer Registration

https://developer.amazon.com

Sign your life away

Developer Registration

https://developer.amazon.com

Choose not to make money :)

Configuring your Skill

Demo

  • Invocation word
  • Intents
  • Sample Utterances
  • Lambda

Interaction Model

Intents

{
 "intents": [ 
   { 
     "intent": "NextEvent", 
     "slots": [] 
   }
 ] 
} 
NextEvent when is the next event
NextEvent when is the next meetup
NextEvent when is BrisJS on next

Sample Utterances

Invocation Word

Bris J S

Lambda Configuration

Developing your Skill

Serverless Framework Config

service: alexa-meetup-skill
provider:
  name: aws
  runtime: nodejs4.3

functions:
  alexa:
    handler: src/alexa.handler
    environment:
      ALEXA_APP_ID: ${env:ALEXA_APP_ID}
      MEETUP_API_URL: https://api.meetup.com
      MEETUP_URL_NAME: ${env:MEETUP_URL_NAME}
    events:
      - alexaSkill

Developing your Skill

Lambda Handler

'use strict';

const Alexa = require('alexa-sdk');
const handlers = require('./handlers');

const APP_ID = process.env.ALEXA_APP_ID;

module.exports.handler = (event, context) => {
    var alexa = Alexa.handler(event, context);
    alexa.appId = APP_ID;
    alexa.registerHandlers(handlers);
    alexa.execute();
};

Developing your Skill

const moment = require('moment');
const meetup = require('../lib/meetup');

module.exports = {
    'NextEvent': function() {
        const emit = this.emit.bind(this);
        meetup.listEvents('upcoming', false).then((events) => {
            const any = events.length > 0;
            if (!any) {
                emit(':tell', 'There are no upcoming meetups');
            } else {
                const time = moment(events[0].time);
                emit(':tell',
                     `The next meetup is on
                      <say-as interpret-as="date">${time.format('YYYYMMDD')}</say-as>.
                      ${time.diff(moment(), 'days')} days from now.`
                );
            }
        })
        .catch(() => {
            emit(':tell', 'Sorry. I am having trouble finding the next meetup.')
        });
    }
};

Intent Handler

Demo Time

Code + Ask BrisJS

Developing your Skill

Taking it up a Level

Developing your Skill

Intents with Slots

{
  "intents": [
    {
      "intent": "RandomKill",
      "slots": [{
      	"name": "Count",
      	"type": "AMAZON.NUMBER"
      }]
    },
    { 
      "intent": "CountInstances"
    },
    {
      "intent": "StartInstances",
      "slots": [{
      	"name": "Count",
      	"type": "AMAZON.NUMBER"
      }]
    },
    {
      "intent": "AMAZON.HelpIntent"
    },
    {
      "intent": "AMAZON.StopIntent"
    }
  ]
}

Developing your Skill

Utterances with Slots

RandomKill run chaos monkey
RandomKill chaos monkey
RandomKill kill {Count} server
RandomKill kill {Count} servers
RandomKill kill {Count} instance
RandomKill kill {Count} instances
RandomKill terminate {Count} server
RandomKill terminate {Count} servers
CountInstances how many servers are running
CountInstances how many instances are running
CountInstances how many e c two instances are running
StartInstances start a server
StartInstances start an instance
StartInstances start an e c two instance
StartInstances run a server
StartInstances run an instance
StartInstances run an e c two instance
StartInstances start {Count} server
StartInstances start {Count} servers
StartInstances start {Count} instance
StartInstances start {Count} instances
StartInstances start {Count} e c two instance
StartInstances start {Count} e c two instances
StartInstances run {Count} server
StartInstances run {Count} servers
StartInstances run {Count} instance
StartInstances run {Count} instances
StartInstances run {Count} e c two instance
StartInstances run {Count} e c two instances

Developing your Skill

Lambda Events

{
  "session": {
    "sessionId": "SessionId.0887154f-ab0c-4db4-9293-9c6e0f3c3247",
    "application": {
      "applicationId": "amzn1.ask.skill.69483f2c-0154-4cf1-a9d9-f7b697c65a54"
    },
    "attributes": {},
    "user": {
      "userId": "amzn1.ask.account.xxxxx"
    },
    "new": true
  },
  "request": {
    "type": "IntentRequest",
    "requestId": "EdwRequestId.2ccc70da-4418-4e22-9244-43fa77e4d7a1",
    "locale": "en-US",
    "timestamp": "2017-03-05T06:35:07Z",
    "intent": {
      "name": "StartInstances",
      "slots": {
        "Count": {
          "name": "Count",
          "value": "3"
        }
      }
    }
  },
  "version": "1.0"
}

Developing your Skill

Handling requests

'use strict';

const chaosService = require('../../chaos-service');
const countHelper = require('./count-text-helper');

module.exports = (intent) => {
    const countSlot = intent.slots ? intent.slots.Count : null;

    const count = countSlot ? countSlot.value : 1;

    return chaosService
        .terminate({ count })
        .then((result) => {
            const terminatingCount = result.terminate &&
                result.terminate.TerminatingInstances ?
                result.terminate.TerminatingInstances.length : 0;

            const countText = result.count ? countHelper.getCountText(result.count) : '';

            const text = terminatingCount > 0 ?
                        `Booooom. You just killed ${terminatingCount} ${terminatingCount === 1 ? 'server' : 'servers'}. ${countText}` :
                        `I didn't kill any servers. ${countText}`;

            return {
                sessionAttributes: {},
                cardTitle: "Kill",
                speechOutput: text,
                repromptText: "",
                shouldEndSession: true
            }
        });
};

Developing your Skill

Lambda Responses

{
  "version": "1.0",
  "response": {
    "outputSpeech": {
      "type": "PlainText",
      "text": "I started 3 servers for you."
    },
    "card": {
      "content": "I started 3 servers for you.",
      "title": "Start",
      "type": "Simple"
    },
    "reprompt": {
      "outputSpeech": {
        "type": "PlainText",
        "text": ""
      }
    },
    "shouldEndSession": true
  },
  "sessionAttributes": {}
}

Developing your Skill

Handling requests

const BbPromise = require('bluebird');
const AWS = require('aws-sdk');

const lambda = new AWS.Lambda({
    region: 'us-east-1' //change to your region
});

const invokeLambda = (lambdaName, event) => {
    const params = {
        FunctionName: lambdaName,
        Payload: JSON.stringify(event),
    };

    return new BbPromise((resolve, reject) => {
        lambda.invoke(params, (err, data) => {
            if (err) {
                reject(err);
                return;
            }
            console.log('Received result: ', data);
            resolve(data);
        });
    });
}

const lambdaResolver = (lambdaName) => 
        (event) => invokeLambda(lambdaName, event).then(
            (data) => JSON.parse(data.Payload)
        );

const countBy = lambdaResolver(process.env.COUNT_LAMBDA);
const start = lambdaResolver(process.env.START_LAMBDA);
const terminate = lambdaResolver(process.env.TERMINATE_LAMBDA);
const stop = lambdaResolver(process.env.STOP_LAMBDA);

module.exports = {
    countBy,
    start,
    terminate,
    stop,
}

Developing your Skill

Causing Chaos

'use strict';

const AWS = require('aws-sdk');
const chaosService = require('./chaos-service');

const ec2 = new AWS.EC2();

// {
//   "count": 3
// }

module.exports.handler = (event, context, callback) => {
    return chaosService
        .terminate(ec2, event.count)
        .then((result) => callback(null, result))
        .catch(err => callback(err));
};

Demo Time

Chaos Alexa

Bonus Level

GraphQL with Serverless & React

type Group {
    key: String
    value: String
}

type Count {
    total: Int!
    groups: [Group!]
}

type Tag {
    Key: String
    Value: String
}

type InstanceState {
    Code: Int!
    Name: String!
}

type InstancePlacement {
    AvailabilityZone: String!
}

type Instance {
    InstanceId: String!
    ImageId: String!
    State: InstanceState!
    Placement: InstancePlacement!
    Tags: [Tag!]
}

enum CountSelector {
    az
    name
    size
    state
}

type Query {
    countBy(selector: CountSelector): Count!
    list(state: String): [Instance!]
}

Bonus Level

GraphQL Resolvers

const BbPromise = require('bluebird');
const AWS = require('aws-sdk');

const lambda = new AWS.Lambda();

const invokeLambda = (lambdaName, event) => {
    const params = {
        FunctionName: lambdaName,
        Payload: JSON.stringify(event),
    };

    return new BbPromise((resolve, reject) => {
        lambda.invoke(params, (err, data) => {
            if (err) {
                reject(err);
                return;
            }
            console.log('Received result: ', data);
            resolve(data);
        });
    });
}

const lambdaResolver = (lambdaName, getEvent) =>
    (args) => invokeLambda(lambdaName, getEvent(args))
                .then((data) => JSON.parse(data.Payload));

const countBy = lambdaResolver(process.env.COUNT_LAMBDA, (args) => ({ selector: args.selector }));
const list = lambdaResolver(process.env.LIST_LAMBDA, (args) => ({ state: args.state }));

module.exports = {
    countBy,
    list,
};

Bonus Level

Dashboard in React

import React from 'react';
import { Doughnut } from 'react-chartjs';

import { graphql } from 'react-apollo';
import gql from 'graphql-tag';

import colorHelper from './color-helper'

const InstancesQuery = gql`query InstanceCount($selector: CountSelector) { 
    countBy(selector: $selector) { 
        total, 
        groups { key, value } 
    } 
}`;

...

const InstancesChart = ({ data }) =>{
    ...
    return (
        <div>
            <Doughnut data={chartData} options={chartOptions} height="200" />
        </div>
    )
}

export default graphql(InstancesQuery, {
    options: ({ selector }) => ({
        pollInterval: 30 * 1000,
        variables: { selector } 
    }),
})(InstancesChart);

Demo Time

Chaos Dashboard

What I Learnt

Alexa skills are different but fun

  • Conversational UI is not simple
  • Writing responses for speech is different to writing for reading
  • Rolling your own SDK for Alexa is easy
  • Backends for Frontends is great for applications with multiple clients

Resources

Reading

  • https://developer.amazon.com/blogs/tag/Alexa
  • https://read.acloud.guru/alexa-champ/home
  • https://github.com/johncmckim/alexa-meetup-skill
  • www.hackster.io/a-cloud-guru/alexa-chaos-monkey-280d79
  • https://github.com/alexa-ops

Alexa Projects

We're Hiring

Front-end and Full-Stack JS Devs

Thanks for Listening!

Questions?

twitter.com/@johncmckim

johncmckim.me

medium.com/@johncmckim

Building Alexa Skills

By John McKim

Building Alexa Skills

  • 139