LoginSignup
33
39

More than 5 years have passed since last update.

Express+alexa-appでlambdaを使わないでAlexa Skillを作るよ

Last updated at Posted at 2017-12-09

この記事は Amazon Alexa Advent Calendar 2017 10日目の記事です。

あと、Node.js Advent Calendar 2017 10日目の記事にしました(空いてたから)

hidesukeです。IT業界から卒業して、製造業な会社でサーバアプリをもりもり書いているフレンズです。わーい、hidesuke、nodejs大好きー。

趣味でやってるかきあげ!っていう小説投稿サイトもnodejs+mongodbで作りました。投稿とか、感想とか超お待ちしております!

この記事のゴール

  • Alexa Skills Kit SDK for Node.jsを使わないでAlexa Skillsのサーバを実装する
  • alexa-appの簡単な使い方をマスターする
  • AWS lambdaを使わないで自前のサーバにデプロイできるようになる。

とりあえず、今回のために作ってサンプルを置いておきますね。

ちなみに前提知識として以下があることを想定しています

  • 知らないとわけがわからない
    • node.js、Expressについて知っている
    • Amazon Developer Consoleを使った事がある。
  • 知ってるともっと楽しめる
    • Alexa Skillsをlambdaを使って作ったことがある

前置き

AlexaのSkillsを作るのはAWS lambdaを使うらしい

ちまたでは「Alexa便利! IFTTTと連携してほげほげした!」みたいな話をよく耳にするのですが、IFTTTで頑張るくらいならAlexa Skills作ろうぜ! って思うくらい Alexa Skills、簡単に作れます。マジ簡単。騙されたとおもって騙されてみるといい。(※ただしnode.js書ける人に限る)

うっしゃ騙されて作ってみるかーーと思って調べ始めるとAlexa Skills Kit SDK for Node.jsというのがAmazon公式から配布されているらしい。こいつを使うと簡単にAlexa Skills作れるし、公式だから安心! よしよしいいじゃないか。

しかも、このAlexa Skills Kit SDKはAWS lambdaを使う前提で作られています。やったぜ今流行のサーバレスアーキテクチャだ! ひゃっほー!

というわけで、ホスティングするサーバのことを気にしなくていい上、話題のAlexa Skillsが作れる。そういえば、アメリカではAlexa Skillsを作って公開したら糞ださい靴下がもらえるキャンペーンとかやってたなぁ……って今確認したらなんかパーカー配ってた。なにこれ超ほしい。

これでいわゆる「勝ったなガハハ」ですよ。

いやだ。絶対に嫌だ。おれは俺のサーバでAlexaするんだ

だがまってほしい。まぁ、個人でやるぶんにはいいです。でもほら、想像してみてください。あなたの上司が「いまAlexaというのが流行ってるそうじゃが、我が社でもAlexaできんのかね。というかヤレ。かんたんじゃろ?」とか言ってきたとします。ああ、なんて流行に敏感な上司なんでしょう! 多分こういう人はAIとか大好きなんでしょうね! AI、いいですよね! なんでもできそう! シンギュラリティはとっくの昔に超えてたんや!! 上司の頭のなかでは。

的な。的な。

で、いざ開発始めてみてとある問題にぶつかるわけです。「あ、うちAWSとか使ってないわ」と。そしてあなたは件の上司に「lambdaを使いたいんですが」と相談をする。すると件の上司「それはいくらかかるんじゃ。何? 我が社にサーバはいっぱいあるじゃろ! そっちを使え! そんな無駄な金だせるわけないじゃろ!」とかいいだすわけですよ。

はい、詰んだー。詰みましたー。

……と、まぁ、そうじゃなくても、ある特定の、社外に公開されていないAPIにつなぐためにアクセス制御が必要で、lambdaのような環境からだとアクセスできないという場合もままあるかと思います。

ああ、つらい。つらすぎる。

そこで alexa-appですよ

究極的に言えば、alexa用のサーバは Amazonから特定のURLにpostでJSONが投げつけられてくる、いわゆるwebhook的なやつです。JSONの仕様は公開されています

なので、これに沿ってそういうサーバを実装して、決められたJSONを返却するようなサーバを書けばいいっちゃいいんですが、そんなこと絶対やりたくないじゃん?

しかも、これ以外にもいろいろやらないといけないことがあって、これ、自分で実装とか絶対無理。やだやだ。

と、まぁ、そんな感じでハゲるほど悩んでいたら発見しました。alexa-appです。

Express + alexa-app

alexa-app

alexa-appが何やってくれるかというと

  • Amazonから飛んでくるrequestをいい感じに処理してくれる
  • Amazonに返すresponseをいい感じに作ってくれる
  • Expressと連携できる

と、大雑把にこんな感じです。Expressと連携できるとか神かよ。

サンプルプログラム

さて、というわけで今回作ったサンプルを見てみましょう。

Express + alexa-app で Alexa Skillsを作ったというただそれだけのサンプルです。Alexaに「ふっふー」というと「ふっふっふー! テンション上がってきたぁぁ!」みたいな超頭の悪い返答をするSkillのサンプルです。公開する予定は全くありません。IQ下げていこう! という気持ちで書きました。

インストール

各モジュールの公式ドキュメントを参照してほしい。こういうのはたいてい npm install モジュール名でうまくいく。

今回のサンプルではpackage.json

package.json
  "dependencies": {
    "alexa-app": "^4.2.0",
    "body-parser": "~1.16.0",
    "cookie-parser": "~1.4.3",
    "debug": "~2.6.0",
    "express": "~4.14.1"
  },

こんな感じでdependenciesを書いてあるので npm installとすれば必要なものが入ります。うっすうっす。(フルバージョン)

app.js

肝はalexa-appとExpressを紐付けることなんです。app.jsを見てみましょう。(フルバージョンはこちら)

app.js
const alexa = require('./routes/alexa');
const app = express();

// alexa-app 用の設定
alexa.express({
  expressApp: app,
  endpoint: '/alexa',
  checkCert: true
});

const alexa = require('./routes/alexa');については後で説明しますが、alexa周りの処理を詰め込んだアンチクショウです。

app.jsの中で expressのオブジェクトを作るの、まぁ、みなさんやるんですが、こいつをalexaオブジェクトに渡してやります。ついでに、endpointも設定してあげましょう。この例だと、AmazonサーバからアクセスすべきwebhookのURLはhttps://ホスト名/alexaになります。あ、SSLじゃないと絶対だめですからね。コレをAmazon Developer Consoleのendpointのところにぶち込んでやりましょう。やったね。

alexa.js

さて、alexa.jsの中でalexaに必要な処理をいろいろやっているんですが、まずはコードを見ましょう。

alexa.js

const Alexa = require('alexa-app');
const app = new Alexa.app('Sample');

const UTTERANCES = {
  response: {
    launch: '「ふっふー」とか「ばっばー」とか言ったらなんか言います。選べ',
    foo: 'ふっふっふー! テンション上がってきたぁぁ!',
    bar: 'あばばばばばばばばば',
    stopAndCancel: 'あ、すいません。退場します。',
  },
  request: {
    stop: ['やめて', 'もうええわ', 'バイバイ'],
    help: ['ボスケテ', 'たすけて'],
    foo: ['ふっふー', 'ふー'],
    bar: ['ばっばー', 'ばばぁ'],
  },
};

const helpResponse = (req, res) => res.say(UTTERANCES.response.launch).shouldEndSession(false);
const stopAndCancelResponse = (req, res) => res.say(UTTERANCES.response.stopAndCancel);
const fooResponse = (req, res) => foobarResponse(req, res, 'foo');
const barResponse = (req, res) => foobarResponse(req, res, 'bar');
const foobarResponse = (req, res, type) => res.say(UTTERANCES.response[type]).shouldEndSession(true);

// 「Alexa〜〜を開いて」と言ってSkillsを起動したときに発動するintent
app.launch((req, res) => {
  res.say(UTTERANCES.response.launch).shouldEndSession(false);
});

// 下記3つは実装しとかないと審査通らないので注意。↑のlaunchも
app.intent('AMAZON.StopIntent', { utterances: UTTERANCES.request.stop }, stopAndCancelResponse);
app.intent('AMAZON.CancelIntent', { utterances: UTTERANCES.request.stop }, stopAndCancelResponse);
app.intent('AMAZON.HelpIntent', { utterances: UTTERANCES.request.help }, helpResponse);

// 「ふっふー」と陽気に問いかけると実行されるやつ
app.intent('foo', { utterances: UTTERANCES.request.foo }, fooResponse);
// 「ばっばー」と不穏に問いかけると実行されるやつ
app.intent('bar', { utterances: UTTERANCES.request.bar }, barResponse);

module.exports = app;

こんだけっす。

requireとか


const Alexa = require('alexa-app');
const app = new Alexa.app('Sample');

ここで、肝になるalexa-appを読み込んで、appという名前オブジェクトを作ってます。new Alexa.app('Sample');ってしてますが、ここの引数はなんでもいいっぽいっす。どこで使ってるんだろう??

受け答えする内容はまとめておくと便利

const UTTERANCES = {
  response: {
    launch: '「ふっふー」とか「ばっばー」とか言ったらなんか言います。選べ',
    foo: 'ふっふっふー! テンション上がってきたぁぁ!',
    bar: 'あばばばばばばばばば',
    stopAndCancel: 'あ、すいません。退場します。',
  },
  request: {
    stop: ['やめて', 'もうええわ', 'バイバイ'],
    help: ['ボスケテ', 'たすけて'],
    foo: ['ふっふー', 'ふー'],
    bar: ['ばっばー', 'ばばぁ'],
  },
};

ここでは、responseとしてAlexaに返す文言、requestとしてAlexaに呼びかけるサンプル文言を定義しています。requestの方に関しては後述しますが、こんな風に受け答えをjson形式で定義しておくといろいろ便利です。ここだけ外出しちゃってもいいですしね。utterance.jsonみたいな感じで。

launchRequest

Alexaに話しかけるときに「アレクサ、ほげもげもげして」みたいに、skill名とやってほしいことを一緒に指定できることはわりと知られています。これをワンショット起動とかって言ったりします。

これとは別に「アレクサ、ほげを開いて」っていうと、Alexaが「はい、ほげを開きます」とか言って待機状態になります。これをダイアログ起動とかって言ったりします。

ダイアログ起動をした場合、AmazonからLaunchRequestというものが飛んできます。コレを受信した場合は、たとえば「ようこそ、バーボンハウスへ。このテキーラはサービスだから、まず飲んで落ち着いて欲しい。」みたいな気の利いたウェルカムメッセージを出してあげる必要があります。

このlaunchRequestを受けるのがapp.launch((req, res) => { /* 処理 */})の部分です。この処理の中で何か気の利いた返しをしてあげましょう。

intentRequest

さて、コードの下の方にapp.intent()というのが並んでいます。コレはintentRequestと言って、一番使うやつです。

そもそも intent って何かっていうと、コマンドの名前みたいなものだと思ってください。Alexaに何か話かけると、Amazonさんのサーバ上でユーザの発話内容がこのintentに変換されて、私たちのサーバに渡されてきます。intentはAmazonで予め作られたintent(AMAZON.StopIntent,AMAZON.CancelIntent,AMAZON.HelpIntentなど)とユーザが定義してAmazon Developer Console上で定義したintent(このアプリの場合はfoo, bar)があります。このintentによって処理を振り分けます。

intentRequestSample.js
app.intent(
  'bar',
  { utterance: ['ばっばー', 'ばばぁ'] },
  (req, res) => { /* 処理 */}
);

上記の例で見ましょう。

第一引数barはintentの名前です。

第二引数{ utterance: ['ばっばー', 'ばばぁ'] }はAmazon Develper Consoleに登録する Sample Utteranceです。空でも構いませんが、コレを指定しておくと後で役に立ちます。

第三引数は実際の処理を書きます。

ね、簡単でしょ?

絶対に実装しないといけないintent

Alexa Skillsを実際にpublishしようとすると、Amazonによる審査があります。そのとき、必ず実装しないといけないintentがあります。それは

  • launch
  • AMAZON.StopIntent
  • AMAZON.CancelIntent
  • AMAZON.HelpIntent

です。これを実装していないと審査は通らないので注意な!

res.say

ここまで、Amazonからrequestがきたときの受け方を見てきました。では、Alexaに言わせる返答はどうすればいいのでしょう?

const foobarResponse = (req, res, type) => {
  res.say('返答だよよよ').shouldEndSession(true);
};

わかりやすいですね!sayです! セイセイセーイ
res.say(返答内容)と書けば返答が送れます。

このshouldEndSessionというのが気になると思います。shouldEndSession(true)の場合、Alexaは返答したあとにセッションを終了します。Echoで見ると、青いリングの光が消える状態ですね。shouldEndSession(false)だと、返答後も青いリングが点きっぱなしの状態になります。つまり、まだユーザからの入力を受け付ける状態です。skills kit SDKで言うところの:ask:tellの違いですね。

ちなみに

res.say('返答だよ').reprompt('どうした。なにか喋れ').shouldEndSession(false);

みないに書いておくと「返答だよ」とAlexaが言ったあと、ユーザが5秒間何も言わなかったら「どうした。何か喋れ」と圧をかけてくるみたいなこともできます。便利。

こんだけ知ってたらもう、簡単だね!

SampleUtteranceとIntentSchemaの自動生成

intentRequestのところで「第二引数はいらない。でも書いといたらいいことある」と書きました。

コレ書いておくとSampleUtteranceとIntentSchemaがコードから自動生成できて超便利なんです。

Amazon Developer ConsoleにはIntentSchemaというintentの定義を登録する場所と、SampleUtteranceというIntentと発話サンプルの組をひたすら登録するフォームがあるんです。どうやって管理すんだよ、地獄かよ。

ということで、さっきのapp.intentの第二引数を定義しておくと以下のスクリプトでSampleUtteranceとIntentSchemaを自動生成できます。

create_schema_for_amazon.js
const fs = require('fs');
const alexa = require('../routes/alexa');

// SampleUtteranceを作成
const utterances = alexa.utterances();
// IntentSchemaを作成
const schema = alexa.schema();

// ファイルに吐く
fs.writeFileSync(`${__dirname}/SampleUtterance.txt`, utterances, err => { if (err) throw err; });
fs.writeFileSync(`${__dirname}/IntentSchema.json`, schema, err => { if (err) throw err; });

生成結果はこんなかんじ

SampleUtterance.txt
AMAZON.StopIntent やめて
AMAZON.StopIntent もうええわ
AMAZON.StopIntent バイバイ
AMAZON.CancelIntent やめて
AMAZON.CancelIntent もうええわ
AMAZON.CancelIntent バイバイ
AMAZON.HelpIntent ボスケテ
AMAZON.HelpIntent たすけて
foo ふっふー
foo ふー
bar ばっばー
bar ばばぁ
IntentSchema.json
{
   "intents": [
      {
         "intent": "AMAZON.StopIntent"
      },
      {
         "intent": "AMAZON.CancelIntent"
      },
      {
         "intent": "AMAZON.HelpIntent"
      },
      {
         "intent": "foo"
      },
      {
         "intent": "bar"
      }
   ]
}

あとは、こいつらをAmazon Developer Consoleにコピペすればおっけー。簡単。

斯くして我々は自鯖でAlexaを運用することに成功した

alexa-app、他にもいろいろできることがあるのですが、まぁ、使いながらドキュメント読みながら覚えてください。

今回の件とはあまり関係ないのですが、alexa-appを使うとUnitTestも簡単にかけるようになります。簡単? かどうかは審議が必要なところですが、少なくとも普通のExpressアプリ的なノリでUnitTestが書けます。alexa-appのtestディレクトリがめちゃめちゃ参考になるのでこちらを参考にしましょう。

というわけで今回はココまで。
ぶっちゃけ、某Skillの開発中にalexa-appを使った例がすごく少なくてググってもほしい答えがなくて、泣く泣くalexa-appのソースコードを読んだりAlexaのJSONの定義を一生懸命紐解いたりと超頑張って辛かったので、この記事が呼び水となってalexa-appの使用者が増え、alexa-appに関する記事が世にあふれるようになることを願いながら、今宵は筆を置くことにする。

とっぴんぱらりのぷー。

33
39
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
39