インライン エディタを使ったAIスピーカー・テンプレートで全問正解クイズが作れるGoogleアシスタント

# はじめに
Google アシスタントのアプリケーションは、どなたでも開発することが出来ます。ここでは、「全問正解クイズ」を例に、設定内容やソースコードの解説を行います。これは、Google アシスタントのテンプレートにもなります。アプリケーションを開発する際は、**【ポイント】**の内容を参考にしてください。

## "OK、Google。南吉の青春クイズにつないで！！"
「全問正解クイズ」の例として、Google アシスタントで、「南吉の青春クイズ」が公開されています。
[南吉の青春クイズ | Google アシスタント - Google Assistant](https://assistant.google.com/services/a/uid/000000715f23ab59?hl=ja)

## 「全問正解クイズ」とは
すべての問題の答えは、すべて同じです。あらかじめ答えがわかっている上で、問題に答えます。
参考: https://ameblo.jp/noboruzark/entry-11328651680.html

## 準備するもの
- [x] Actions on Google の開発者アカウント
- [x] クイズの答えと、問題および解説

今回の例では、すべての問題の答えを**「新美南吉」**としました。

## 開発の手順
1. Actions on Google でプロジェクトを作成
1. Dialogflow でエージェントを開発
    1. Entities の設定
    1. Intents の設定
    1. Fulfillment の編集

# 設定と編集
Actions on Google や Dialogflow の使い方は、別途、開発者向けチュートリアルを参考にしてください。
ここでは、"Entities の設定"、"Intents の設定"、"Fulfillment の編集"について解説します。

※ 完全な設定内容や編集内容は、[GitHub](https://github.com/jp-96/google-assistant-dialogflow-simplequiz) を参照してください。
GitHub: https://github.com/jp-96/google-assistant-dialogflow-simplequiz

## Entities の設定
Entities では、ユーザーの発話として受け付ける単語を設定します。ユーザーの発話は、Dialogflow で解析され、最適な Entity が選択されます。
設定するのは次の３つのエンティティです。

1. AnswerEntity
1. YesEntity
1. StopEntity

![Entities.png](https://qiita-image-store.s3.amazonaws.com/0/252891/d4b2dc34-fa41-b6ba-4545-2adb6a3ebce2.png)

### AnswerEntity
ますは、問題の答えである**「新美南吉」**を AnswerEntity として設定します。
ここで、"にいみ"には、"新見"や"新実"などの漢字も苗字として存在するようですが、このエンティティで、"新美"として設定しておくと、"新美"という漢字に認識変換されます。
**【ポイント】** ここで設定する単語は、開発するアプリケーションの答えに合わせてください。
![AnswerEntity.png](https://qiita-image-store.s3.amazonaws.com/0/252891/0b1ce5fb-f574-9eb6-38e9-4ac1190f9192.png)

### YesEntity
クイズを開始するかどうかを初めにユーザーに答えてもらうために、YesEntity を設定します。
「イエス」、「はい」、「OK」、「オッケー」の類似語を設定していますので、これらの発話も YesEntity として認識されます。
![YesEntity.png](https://qiita-image-store.s3.amazonaws.com/0/252891/af3f470f-073d-7777-f038-4270f4898daf.png)

### StopEntity
ユーザーが途中でやめる場合に備えて、StopEntity を設定します。強制的に終了する手段がGoogleアシスタントには備わっている為、StopEntity の設定は必須ではありません。
![StopEntity.png](https://qiita-image-store.s3.amazonaws.com/0/252891/6de49cb3-fd7e-c6ca-f6f9-c6254f36cdf8.png)

## Intents の設定
Intents では、Entities で設定した単語をどのように処理するかを設定します。User Says に、その Intent として処理したいユーザーが読み上げる文章例を設定します。Entities で設定された単語が含まれると、その単語は、自動的にパラメータとして登録されます。パラメータは、手動での編集も可能です。
Default Welcome Intent と Default Fallback Intent はあらかじめ設定されていますので、それらはそのまま使用します。その他の Answer Intent、Yes Intent、Stop Intent を追加設定します。

1. Default Welcome Intent (既存)
1. Default Fallback Intent (既存)
1. Answer Intent
1. Yes Intent
1. Stop Intent
![Intents.png](https://qiita-image-store.s3.amazonaws.com/0/252891/c16230b8-e22d-5f84-53c4-02f465940916.png)

### Answer Intent
「新美南吉」という"答え"を認識すると、Answer Intent として処理されます。ユーザーが単語だけを答えることを想定している為、User Says にパラメーターとなる単語のみを設定しています。
**【ポイント】** 開発するアプリケーションの答えを変更しても、Answer Intentを変更する必要は**ありません。**
![AnswerIntent.png](https://qiita-image-store.s3.amazonaws.com/0/252891/0a23ae67-f03b-9301-473c-01e624ad2108.png)

### Yes Intent
「イエス」に対して、Yes Intent として処理されるように設定します。YesEntity で類義語を設定していますので、「イエス」だけでなく、「はい」、「OK」、「オッケー」でも、Yes Intent として処理されます。
![YesIntent.png](https://qiita-image-store.s3.amazonaws.com/0/252891/4e2a2432-53bf-d76c-577b-160443431f12.png)

### Stop Intent
「ストップ」に対して、Stop Intent として処理されるように設定します。StopEntity と同様、Stop Intent の設定も必須ではありません。

## Fulfillmentの編集
Fulfilment で、Inline Editor を Enabled にします（有効化）。index.js ファイルのソースコードを編集し、設定した Intents に対する振る舞いを実装します。
![Fulfillment.png](https://qiita-image-store.s3.amazonaws.com/0/252891/ab3444cc-b5e8-43c9-f031-72694439d8f2.png)

### require部
まずは、"おまじない"を記述します。

```js
// See https://github.com/dialogflow/dialogflow-fulfillment-nodejs
// for Dialogflow fulfillment library docs, samples, and to report issues
'use strict';

const functions = require('firebase-functions');
const {WebhookClient} = require('dialogflow-fulfillment');
const {Card, Suggestion} = require('dialogflow-fulfillment');

process.env.DEBUG = 'dialogflow:debug'; // enables lib debugging statements

```

### 答えと問題の定義
答え(ANSWER)と問題と解説(data)を定数で定義します。NUMBER_OF_QUESTIONS で定義した数だけを、ランダムに選択された要素から順番に出題されます。
**【ポイント】**定数 ANSWER の値は、AnswerEntity で設定した単語と同じ値を定義します。
**【ポイント】**'もんだい'と'かいせつ'は、１組以上、定義してください。１組でも２組でも実行自体には問題ありません。

```js
// 実施する問題数
const NUMBER_OF_QUESTIONS = 3;

// 全問正解クイズの答え
const ANSWER = '新美南吉';

// 問題と解説
// 出典：新美南吉のまちづくり（安城市）
// - https://www.city.anjo.aichi.jp/shisei/nankichi/nankichinomachidukuri.html
// - https://www.city.anjo.aichi.jp/shisei/koho/20120401/documents/p08p11.pdf
const data = [
    {
        'もんだい':"昭和13年4月、24歳の時に安城高等女学校に教師として赴任し、英語や国語などを担当した『ごんぎつね』などで知られる童話作家は誰？",
        'かいせつ':"新美南吉は、また、着任時に入学した第19回生の担任として、卒業までの4年間を受け持ちました。",
    },
    {
        'もんだい':"昭和16年10月には初の単行本『良寛物語 手毬と鉢の子』が出版され、幼い頃からの夢であった童話作家となったのは誰？",
        'かいせつ':"昭和14年4月からは現在の安城市新田町で下宿を始め、昭和16年10月には初の単行本『良寛物語 手毬と鉢の子』が、昭和17年10月には初の童話集『おぢいさんのランプ』が出版されました。",
    },
    {
        'もんだい':"ある童話作家は、29歳という若さで亡くなってしまいましたが、その童話作家が安城で過ごした5年間は、教員という社会的地位を得て経済的に安定し、さらに教え子や同僚たちとの交流から精神的にも充実していました。その童話作家とは誰？",
        'かいせつ':"安城時代は、新美南吉が短い生涯の中で最も輝いた青春時代なのです。安城市は、「南吉が青春を過ごしたまち 安城」をキャッチフレーズとし、南吉のまちづくり事業を推進しています。",
    },
    {
        'もんだい':"大正2年7月30日、愛知県知多郡半田町（現半田市）で生まれ、４歳で母を亡くし、８歳で家族と別れて、母方の実家「新美家」の養子に入るなど、複雑な少年時代を送った童話作家は誰？",
        'かいせつ':"複雑な少年時代を送った南吉（本名：正八）は、この頃の孤独や母のぬくもりへの憧れは、後の作品の中に垣間見ることができます。",
    },
    {
        'もんだい':"１８歳の頃から、若手作家の登竜門であり、日本を代表する児童雑誌の『赤い鳥』に作品を投稿するようになった童話作家は誰？",
        'かいせつ':"昭和６年５月号に童謡「窓」が初入選、以後次々に掲載されます。代表作「ごん狐」も、昭和７年１月号に掲載されました。そして、この頃からペンネーム「南吉」が定着していきました。",
    },
    {
        'もんだい':"昭和７年、東京外国語学校英語部文科に入学。東京では、『赤い鳥』を通じて、巽聖歌や与田凖一、江口榛一などとの出会った童話作家は誰？",
        'かいせつ':"東京での巽聖歌や与田凖一、江口榛一などとの交流を通じて、「新美南吉」の名は、児童文学の世界で徐々に認められていきました。",
    },
    {
        'もんだい':"25歳のとき、中学時代の恩師の尽力により、安城高等女学校の教員になることが決まった童話作家は誰？",
        'かいせつ':"順調にみえた東京生活ですが、昭和11年、体調を崩し帰郷することになります。その後、会社勤めをしますが肌にあわず、また、経済的にも苦しかったようです。",
    },
    {
        'もんだい':"安城高等女学校に赴任した翌年、東京外国語学校時代の友人で、新聞記者の江口榛一からの原稿依頼をきっかけに「最後の胡弓弾き」など、次々に作品を執筆した童話作家は誰？",
        'かいせつ':"昭和16年に初の単行本『良寛物語手毬と鉢の子』が、さらにその翌年には、童話集『おぢいさんのランプ』が出版されました。",
    },
    {
        'もんだい':"喉頭結核が原因で、昭和18年3月22日、29歳と７カ月で亡くなった童話作家は誰？",
        'かいせつ':"昭和23年、南吉をしのんだ元同僚や教え子たちによって、安城高等女学校の中庭に南吉の詩が刻まれた「ででむし詩碑」が建てられました。南吉の顕彰碑第１号です。",
    },
    {
        'もんだい':"南吉サルビーは、「ごんぎつね」や「手ぶくろを買いに」など、ある童話作家の代表作品に登場するキツネをイメージしたキャラクターです。その胸には、旧安城高等女学校のある童話作家の教え子たちが建てた「ででむし詩碑」にちなみ、カタツムリのマークがついています。ある童話作家とは誰？",
        'かいせつ':"安城市の「新美南吉のまちづくり」を盛り上げることを目的として、「南吉サルビー」を商品や商品パッケージ等において無償でご利用いただくことが可能です。",
    },
];

```

### 定型文
問題や解説以外の応答は、定型文で返しますので、それらを定数として定義します。
**【ポイント】** 定数 WELCOME_MESSAGE は、開発するアプリケーション名に合わせてください。

```js
// 定型文
const WELCOME_MESSAGE = "南吉の青春クイズへようこそ！";
const HELP_MESSAGE = "すべての問題の答えは、「" + ANSWER + "」です。";
const READY_MESSAGE = "問題をよく聞いてから答えてください。始めてもよろしければ、「はい」とお答えください。";
const START_MESSAGE = "それでは、始めます。";
const NEXT_MESSAGE = "<break strength='strong'/>全問正解クイズ<break strength='strong'/>【問題】<break strength='strong'/>";
const CORRECT_MESSAGE = "正解！<break strength='strong'/>解説します。";
const EXIT_MESSAGE = "また挑戦してくださいね。";
const REPROMPT_MESSAGE = "よく聞き取れませんでした。もう一度、どうぞ。";
const COMPLETE_MESSAGE = "あなたは、素晴らしい！見事、全問正解です。こんなこと初めてです。それでは、また、お会いしましょう。さようなら！";
```

### ヘルパー関数
乱数や返事を取得できる便利関数を定義しています。

```js
//=========================================================================================================================================
// helper functions.
//=========================================================================================================================================

function getRandom(min, max)
{
    return Math.floor(Math.random() * (max-min+1)+min);
}

//This is a list of positive speechcons that this skill will use when a user gets a correct answer.  For a full list of supported
//speechcons, go here: https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/speechcon-reference
const speechConsCorrect = ["あっはっは", "あら", "あらあ", "イェイ", "うっひゃあ", "うっひょう", "うふふ", "おー", "おおー", "おっ",
"おめでとう", "乾杯", "そうそう", "は〜い", "はっはっは", "万歳", "ピンポーン", "ほ〜", "やったあ",
"やっほう", "ようし", "わ〜い", "わあーっ", "わっしょい"];

//This is a list of negative speechcons that this skill will use when a user gets an incorrect answer.  For a full list of supported
//speechcons, go here: https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/speechcon-reference
const speechConsWrong = ["あ〜あ", "あいたた", "あちゃあ", "あっとー", "ありゃ", "あれれ", "うぅ", "ううっ", "え〜", "おっと", "およよ", 
"ぎゃあ", "しくしく", "とほほ", "ドンマイ", "ひいっ", "ぶう", "むっ"];

function getSpeechCon(type)
{
    if (type)
        return "<break strength='strong'/><say-as interpret-as='interjection'>" + speechConsCorrect[getRandom(0, speechConsCorrect.length-1)] + "! </say-as><break strength='strong'/>";
    else
        return "<break strength='strong'/><say-as interpret-as='interjection'>" + speechConsWrong[getRandom(0, speechConsWrong.length-1)] + " </say-as><break strength='strong'/>";
}
```

### 呼び出し部
呼び出されるメイン関数を定義します。
本アプリケーションでは、"開始待ち"と"回答待ち"の２つの状態管理をしていますので、その定数 STATES を定義しています。

```js
//=========================================================================================================================================
// Dialogflow fulfillment
//=========================================================================================================================================

// 開始待ちと回答待ち
const STATES = {
    START:"_START",
    QUIZ:"_QUIZ" 
};

exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
    const agent = new WebhookClient({ request, response });
    let conv = agent.conv();
    console.log('Dialogflow Request headers: ' + JSON.stringify(request.headers));
    console.log('Dialogflow Request body: ' + JSON.stringify(request.body));

```

### 処理部
初期化からクイズの開始、応答、終了まで、状態遷移しながら処理します。

```js
    // 初期化
    const init = function() {
        conv.data = {
          state: STATES.START,
          counter: 0,
          index: 0,
        };
        const speechOutput = WELCOME_MESSAGE + HELP_MESSAGE + READY_MESSAGE;
        conv.ask('<speak>' + speechOutput + '</speak>');
        agent.add(conv);
    };
    // クイズの開始
    const startQuestion = function(){
        conv.data.state= STATES.QUIZ;
        conv.data.counter = 0;
        conv.data.index = getRandom(0, data.length-1);
        const response = START_MESSAGE + HELP_MESSAGE;
        nextQuestion(response);
    };
    // 次の問題へ（実施する問題数を終えたなら閉じる）
    const nextQuestion = function(response){
        const mode = conv.data.mode;
        if (NUMBER_OF_QUESTIONS > conv.data.counter) {
            const dataIndex = (conv.data.index + conv.data.counter) % data.length ;
            const dataItem = data[dataIndex];
            const speechOutput = response + NEXT_MESSAGE + dataItem['もんだい'];
            conv.ask('<speak>' + speechOutput + '</speak>');
            agent.add(conv);
        } else {
            const speechOutput = response + getSpeechCon(true) + COMPLETE_MESSAGE;
            conv.close('<speak>' + speechOutput + '</speak>');
            agent.add(conv);
        }
    };
    // 正解
    const correntAnswer = function() {
        const dataIndex = (conv.data.index + conv.data.counter++) % data.length ;
        const dataItem = data[dataIndex];
        const response = CORRECT_MESSAGE + dataItem['かいせつ'];
        nextQuestion(response);
    };
    // 不正解
    const incorrentAnswer = function() {
        const speechOutput = getSpeechCon(false) + HELP_MESSAGE;
        conv.ask('<speak>' + speechOutput + '</speak>');
        agent.add(conv);
    };
    // ヘルプ
    const help = function() {
        const speechOutput = HELP_MESSAGE + READY_MESSAGE;
        conv.ask('<speak>' + speechOutput + '</speak>');
        agent.add(conv);
    };
    // 中断（閉じる）
    const bye = function() {
        const speechOutput = EXIT_MESSAGE;
        conv.close('<speak>' + speechOutput + '</speak>');
        agent.add(conv);
    };
```

### Intentsの振る舞い部
Intentsに対するそれぞれの振る舞いを処理します。Intentsと振る舞いとは、Mapインスタンスで関連付けを定義し、エージェントに設定しています。
Intents は、状態遷移におけるトリガーだと考えると理解しやすいと思います。

```js
    // Run the proper function handler based on the matched Dialogflow intent name
    function welcome(agent) {
        init();
    }
    
    function fallback(agent) {
        if (STATES.START === conv.data.state)
        {
            help();
        }
        else
        {
            incorrentAnswer();
        }
    }
    
    function yes(agent) {
        if (STATES.START === conv.data.state) {
            startQuestion();
        }
        else
        {
            incorrentAnswer();
        }
    }
    
    function answer(agent) {
        if (STATES.QUIZ === conv.data.state) {
            if (ANSWER === agent.parameters.AnswerEntity)
            {
                correntAnswer();
                return;
            }
        }
        incorrentAnswer();
    }
    
    function stop(agent) {
        bye();
    }
    
    let intentMap = new Map();
    intentMap.set('Default Welcome Intent', welcome);
    intentMap.set('Default Fallback Intent', fallback);
    intentMap.set('Yes Intent', yes);
    intentMap.set('Answer Intent', answer);
    intentMap.set('Stop Intent', stop);
    agent.handleRequest(intentMap);
});
```

# Amazon Alexa Skill(Amazon版)
Amazon Alexa Skill のサンプルもソースコードだけですが、ご紹介します。
https://github.com/jp-96/skill-sample-nodejs-simplequiz/blob/master/README.md

# おわりに
作成したアプリケーションは、実機でテストすることが出来ます。また、アプリケーションを申請し、審査に通過すれば、誰でも Google アシスタントからそのアプリケーションを利用することが出来るようになります。まずは、「全問正解クイズ」を申請してみてはいかがでしょうか。

# 最後に？
このアプリケーションを応用した例が、「うんこパンツ検定」です。
「うんこパンツ検定」では、２つのモードを備えており、モードを選ぶことが出来ます。
隠しコマンドも備えていますので、探し出してみてください。
それでは、さようおなら、「プっ！」

[うんこパンツ検定 | Google アシスタント - Google Assistant](https://assistant.google.com/services/a/uid/00000039ea22b626?hl=ja)