Implementing an Alexa skill requires a free developer account with Amazon as well as a secure web service. It is easier by far to let Amazon handle this with Amazon Web Services Lambda. Opening an AWS account requires entering a credit card number, but Amazon says explicitly this service is free for most developers.

Documentation for Alexa Skills Kit (ASK):

https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit

Skills are run by waking Alexa and uttering one of the supported launch words followed by the invocation name. Initially supported launch words include

Ask
Begin
Launch
Load
Open
Resume
Run
Start
Talk to
Tell
Use

Basic Steps for Implementing a Skill

  1. Create a Lambda function in Amazon Web Services
  2. Enter debugged code in the online form or upload a .ZIP file of code with required Node.js modules
  3. Add Alexa Skills Kit as an event source for the function in the event sources tab and enable the source
  4. Copy the Amazon Resource Name (ARN) of the function
  5. Add a new skill in the Alexa section of apps in the Amazon Developer console
  6. Paste in the ARN of the AWS Lambda function when creating the new skill to link it with the AWS Lambda function
  7. Include a valid intent schema and sample utterances to enable the skill for testing
  8. The skill will appear in the Skills section of the Echo app if your developer account is linked to your main Amazon account

Hello World — Minimal Code

The samples provided by Amazon include a “Hello World” skill, but it is overly dense for what is meant to be the simplest program in any language. Very little code is actually needed to implement a traditional program:

exports.handler = function( event, context ) {
    
    var response = {
        outputSpeech: {
            type: "PlainText",
            text: "Hello World"
        },
        card: {
            type: "Simple",
            title: "Hello World",
            content: "Alexa Skills Kit"
        },
        shouldEndSession: true
    };
    
    context.succeed( { response: response } );
    
};

This could technically be written on one line by putting the JSON response directly into the Node.js succeed method but would lose legibility.

Every skill requires an intent schema with at least one defined intent. The following minimal schema is sufficient for a functional skill:

{ "intents": [ { "intent": "Help", "slots": [] } ] }

Apparently intents need not include the word “intent” in their names. This minimal schema can be accompanied by the minimal sample utterance

Help help

and Alexa will respond appropriately when the skill is launched.


Who’s Your Daddy Now?

Although Amazon says explicitly to avoid invocation names that overlap with built-in services, it can be fun to do so. The “Hello World” code can be used to return any desired response, as for example in response to the above invocation name:

exports.handler = function( event, context ) {
    
    var response = {
        outputSpeech: {
            type: "PlainText",
            text: "(gratuitous response from Alexa)"
        },
        card: {
            type: "Simple",
            title: "Who's Your Daddy Now?",
            content: "(gratuitous response from Alexa)"
        },
        shouldEndSession: true
    };
    
    context.succeed( { response: response } );
    
};

The same minimal intent schema and sample utterance can be used for a functional skill. Be aware that deliberately overlapping with built-in services can lead to erratic behavior depending on how well the invocation name is articulated.


Stock Quotes

A skill of this sort needs an online data source, which in this case is the public version of Yahoo Query Language. First the appropriate URL is constructed from the array of stock symbols, then the data in HTTP GET response is assembled and parsed as JSON that can be used to construct a spoken response.

The third-party Node.js request module could be used here for convenience in handling error messages, but that would require uploading a .ZIP file as opposed to this simple solution in a single file. The code for the output is the same as the last two examples, and has been separated as its own function to indicate this.

exports.handler = function( event, context ) {
    
    var stocks = [ 'AAPL', 'GOOG', 'AMZN' ];

    var http = require( 'http' );
 
    var url = 'http://query.yahooapis.com/v1/public/yql';
    url += '?q=select * from yahoo.finance.quotes where symbol in (';
    url += '"' + stocks + '"'; 
    url += ')&env=store://datatables.org/alltableswithkeys&format=json';

    http.get( url, function( response ) {
        
        var data = '';
        
        response.on( 'data', function( x ) { data += x; } );

        response.on( 'end', function() {

            var json = JSON.parse( data );

            var text = 'Here are your stock quotes: ';

            for ( var i=0 ; i < stocks.length ; i++ ) {
                var quote = json.query.results.quote[i];
                if ( quote.Name ) {
                    text += quote.Name + ' at ' + quote.Ask
                            + ' dollars, a change of '
                            + quote.Change + ' dollars. ';
                }
            }
        
            output( text, context );
        
        } );
        
    } );
    
};
    
function output( text, context ) {

    var response = {
        outputSpeech: {
            type: "PlainText",
            text: text
        },
        card: {
            type: "Simple",
            title: "Stocks",
            content: text
        },
        shouldEndSession: true
    };
    
    context.succeed( { response: response } );
    
}

The same minimal intent schema and sample utterance as for the previous two examples can again be used for a functional skill. The format of Alexa’s spoken response can be altered to taste. Having periods at the end of each quote provides a slight pause between quotes.

For reference the available financial fields via YQL are

AfterHoursChangeRealtime
AnnualizedGain
Ask
AskRealtime
AverageDailyVolume
Bid
BidRealtime
BookValue
Change
Change_PercentChange
ChangeFromFiftydayMovingAverage
ChangeFromTwoHundreddayMovingAverage
ChangeFromYearHigh
ChangeFromYearLow
ChangeinPercent
ChangePercentRealtime
ChangeRealtime
Commission
Currency
DaysHigh
DaysLow
DaysRange
DaysRangeRealtime
DaysValueChange
DaysValueChangeRealtime
DividendPayDate
DividendShare
DividendYield
EarningsShare
EBITDA
EPSEstimateCurrentYear
EPSEstimateNextQuarter
EPSEstimateNextYear
ErrorIndicationreturnedforsymbolchangedinvalid
ExDividendDate
FiftydayMovingAverage
HighLimit
HoldingsGain
HoldingsGainPercent
HoldingsGainPercentRealtime
HoldingsGainRealtime
HoldingsValue
HoldingsValueRealtime
LastTradeDate
LastTradePriceOnly
LastTradeRealtimeWithTime
LastTradeTime
LastTradeWithTime
LowLimit
MarketCapitalization
MarketCapRealtime
MoreInfo
Name
Notes
OneyrTargetPrice
Open
OrderBookRealtime
PEGRatio
PERatio
PERatioRealtime
PercebtChangeFromYearHigh
PercentChange
PercentChangeFromFiftydayMovingAverage
PercentChangeFromTwoHundreddayMovingAverage
PercentChangeFromYearLow
PreviousClose
PriceBook
PriceEPSEstimateCurrentYear
PriceEPSEstimateNextYear
PricePaid
PriceSales
SharesOwned
ShortRatio
StockExchange
Symbol
TickerTrend
TradeDate
TwoHundreddayMovingAverage
Volume
YearHigh
YearLow
YearRange

Financial information from the public version of YQL is delayed by about fifteen minutes.


Dow Jones Average

The YQL databases do not include the Dow Jones Industrial Average, presumably due to licensing issues. The web page from Yahoo however has distinctive identifiers for the DOM elements containing the relevant values for the average. Rather than parsing the string retrieved from the URL as a DOM object, it is simpler to apply a regular expression to extract the required data.

exports.handler = function( event, context ) {
    
    var http = require( 'http' );
 
    var url = 'http://finance.yahoo.com/q?s=^DJI';

    http.get( url, function( response ) {
        
        var data = '';
        
        response.on( 'data', function( x ) { data += x; } );

        response.on( 'end', function() {

            var pattern = new RegExp( '"yfs_l10_\\^dji">(.*?)<.*?alt="(.*?)'
                                + '">(.*?)<.*?"yfs_p20_\\^dji">\\(?(.*?)\\)?<' );
            var result = pattern.exec( data );

            var text = 'The Dow Jones Industrial Average is ' + result[1];
            text += '. It is ' + result[2].toLowerCase();
            text += ' ' + result[3].trim() + ' points, ';
            text += 'a change of ' + result[4] + '.';
        
            output( text, context );
        
        } );
        
    } );
    
};
    
function output( text, context ) {

    var response = {
        outputSpeech: {
            type: "PlainText",
            text: text
        },
        card: {
            type: "Simple",
            title: "Dow Jones",
            content: text
        },
        shouldEndSession: true
    };
    
    context.succeed( { response: response } );
    
}

The same minimal intent schema and sample utterance can again be used for a functional skill.


Ask Wolfram

A PHP version of a skill to request information from WolframAlpha has inspired this version entirely in JavaScript. It uses the WolframAlpha API that is free for 2000 requests per month. It requires a free AppID that can be generated in the My Apps section of the Wolfram developer portal.

The skill can be invoked with and without a question. If no question is initially supplied, one will be prompted. The skill remains active for additional questions.

Node.js code for the AWS Lambda function:

exports.handler = function( event, context ) {

    if ( event.request.intent ) {

        question = event.request.intent.slots.Question.value;
        getXML( question, context );

    } else output( 'What is your question?', context );

};

function getXML( input, context ) {

    var http = require( 'http' );
    
    var url = 'http://api.wolframalpha.com/v2/query?input=' + input;
    url += '&format=plaintext&podindex=1,2,3';
    url += '&appid=XXXXXX-XXXXXXXXXX';

    var request = http.get( url, function( response ) {
        
        var xml = '';
        
        response.on( 'data', function( x ) { xml += x; } );
        
        response.on( 'end', function() {
            
            xml = xml.replace( /\n/g, ' ' );

            var pattern = /plaintext>(.*?)<\/plaintext/g;
            var answers = [];
            
            while ( ( match = pattern.exec( xml )) !== null ) {
            
                answers.push( match[1] );
                
            }

            if ( answers[1] === '' ) output( answers[2], context );
            else output( answers[1], context );
        
        } );
        
    } );
  
    request.setTimeout( 4000, function() {
        
        output( 'Your request has timed out. Please try again.', context );
        
    } );
 
}
    
function output( text, context ) {

    var response = {
        outputSpeech: {
            type: "PlainText",
            text: text
        },
        card: {
            type: "Simple",
            title: "Wolfram",
            content: text
        },
        shouldEndSession: false
    };
    
    context.succeed( { response: response } );
    
}

The URL request to the API restricts the returned XML to the top three pods to reduce response time, but you will still probably need to increase the timeout of the AWS Lambda function for a functional skill. The first pod always contains the original question and could be omitted. The second pod generally contains the best answer but will have no plain text response if the top hit is an image. In that case the third pod is used as the response from Alexa.

A minimal intent schema is

{
  "intents": [ {
    "intent": "WhatIs",
    "slots": [ {
       "name": "Question",
       "type": "LITERAL"
    } ]
  } ]
}

Some sample utterances are

WhatIs {what is the atomic weight of boron | Question}
WhatIs {when is sunset tonight | Question}
WhatIs {how tall is mount everest | Question}

The skill cannot be saved if there is any white space between the curly brackets and adjacent words.


Online Etymologies

For students of the English language, one of the best online sources for etymologies is etymonline.com, a rather comprehensive etymological dictionary. For those of us who still read books in traditional paper form (shocking!) it can be convenient to have Alexa perform the lookup.

The site’s server-side script returns searches as HTML description lists. The first etymology on the page is easily retrieved with a nonglobal regular expression. A bit of processing of the response is performed before applying the expression to make the result consistent. Additional processing is done after application of the regular expression to make the output more intelligible when read aloud.

exports.handler = function( event, context ) {

    if ( event.request.intent ) {
        
        word = event.request.intent.slots.Word.value;
        getHTML( word, context );
        
    } else output( 'Which word shall I look up?', context );
    
};

function getHTML( input, context ) {

    var http = require( 'http' );
    
    var url = 'http://etymonline.com/index.php?search=' + input;

    http.get( url, function( response ) {
        
        var html = '';
        
        response.on( 'data', function( x ) { html += x; } );
        
        response.on( 'end', function() {
            
            if ( html.indexOf( '<dd' ) > 0 ) {
       
                html = html.replace( /\n/g, ' ' );
                html = html.replace( /\r/g, ' ' );
                html = html.replace( /\"/g, '' );

                var pattern = /<dd class=highlight>(.*?)<\/dd/;
                var match = pattern.exec( html );
                var def = match[1].replace( /<\/?.*?>/g, '' );
         
                def = def.replace( / c\./g, ' circa' );
                def = def.replace( /\(c\./g, '(circa' );
                def = def.replace( /c\./g, ' century' );
                def = def.replace( /n\./g, 'noun' );
                def = def.replace( /v\./g, 'verb' );

                def = def.replace( /\s\s+/g, ' ' );
      
                output( input + ': ' + def, context );
        
            } else output( 'The word ' + input + 'was not found.' );
            
        } );
        
    } );
  
}
    
function output( text, context ) {

    var response = {
        outputSpeech: {
            type: "PlainText",
            text: text
        },
        card: {
            type: "Simple",
            title: "Etymonline",
            content: text
        },
        shouldEndSession: false
    };
    
    context.succeed( { response: response } );
    
}

Be aware that four occurrences of < in the code have been escaped with &lt; to appear correctly within the <pre> tags. These should present no problems when copying from a browser window as opposed to the page source.

The remaining elements of the skill require more thought than previous examples. Alexa does not hear “etymonline” consistently, so an invocation name of “etymology” is recommended. The intent schema is similar to that for the WolframAlpha lookup:

{
  "intents": [ {
    "intent": "Lookup",
    "slots": [ {
       "name": "Word",
       "type": "LITERAL"
    } ]
  } ]
}

Sample utterances do not work consistently on first invocation without a certain number of words. The first two examples here are the most consistent:

Lookup to look up the word {cat|Word}
Lookup to look up the word {dog|Word}
Lookup to look up {cat|Word}
Lookup to look up {dog|Word}
Lookup the word {cat|Word}
Lookup the word {dog|Word}
Lookup {cat|Word}
Lookup {dog|Word}

Since the skill remains active for additional words, the last two can be used in the context of a second search. Attempting to end the skill with the word “quit” will result in a long etymology of that word. The word “exit” is most reliable for initiating a SessionEndedRequest.


Uploaded 2015.06.27 — Updated 2015.07.20 analyticphysics.com