DialogFlowで作るFacebookメッセンジャー、LINE、Slack、GoogleAssistant対応のチャットボットを作成する

# はじめに
　DialogFlowと出会い、チャットボット開発が面白くなって、ここ数日、暇があれば開発方法について調べていました。
　その時、Qiitaで掲載されている方の記事のおかげでチャットボットが作成できたので、恥ずかしながらアップさせていただくことにしました。
　何かしらどなたかの助けになれば幸いです。

# ボットの紹介
## キャラクター
うさ仙人![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/68e0ccf4-9e12-fd7c-6ba9-ba4072598c5c.png)
[クラフトホリック](http://www.shop-craftholic.com/)のウサギ型宇宙人ですがうちでは仙人風のしゃべり方をさせます。
(/ω＼)
　
## 内容
英単語ゲームができるようにしました。

## 会話の流れ
|FB Messenger|       LINE       |SLACK           |
|:----------:|:----------------:|:--------------:|
|![jvnyg-2ej4d.gif](https://qiita-image-store.s3.amazonaws.com/0/115627/eb70640c-e712-d56e-b5a0-69290a786640.gif)|![dpoz1-54b01.gif](https://qiita-image-store.s3.amazonaws.com/0/115627/af64e89c-3543-fb8e-1049-ad0aa5a02bf9.gif)|![73c2m-pdyr2.gif](https://qiita-image-store.s3.amazonaws.com/0/115627/509051b9-1945-f0d5-fcb6-e4f6b1486222.gif)
|
# 準備

## 使用するサービス
・DialogFlow
　LINEやSLACKなどのチャットツールと連携してメッセージを送受信してくれます。

・Actions on google
　google assistantと連携するのに必要。

・LINE Developer
　DialogFlowと連携するのに必要

・Slack
　DialogFlowと連携するのに必要


# 大まかな流れ

肝となるチャットツールとの連携はDialogFlowがやってくれます。

こんな感じ↓
　LINEなどのチャットツール→DialogFlow→Firebaseプログラム

# 開発手順

## 設計

　アプリケーション開発と同じで設計が重要です。何度も何度もインテントを書き直しました。
　ほんとしておかないと後悔します。

[Dialogflowで少しずつ作りこみには限界がある](https://qiita.com/miyasita1958/items/dbf74125f68533f19dba#%EF%BD%91%EF%BC%94%EF%BC%90dialogflow%E3%81%A7%E5%B0%91%E3%81%97%E3%81%9A%E3%81%A4%E4%BD%9C%E3%82%8A%E3%81%93%E3%81%BF%E3%81%AB%E3%81%AF%E9%99%90%E7%95%8C%E3%81%8C%E3%81%82%E3%82%8B)
  この記事を参考にさせていただきました。

## DialogFlow

　先にこちらの記事を読むことをおすすめします。
　[サルにもわかる Dialogflow FAQ](https://qiita.com/miyasita1958/items/dbf74125f68533f19dba)

### インテント
![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/53adca8e-04ef-e2be-f891-acd4c04499b6.png)

#### Default Welcome Intent
![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/3f4e7425-b558-b073-5310-b847b00b13cb.png)
　MainMenuコンテキストをセットしてメインメニューを表示している状態を他のインテントに認識させます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/0be451f2-da4e-366f-4850-b11298a63a2f.png)
　Welcomeイベントはデフォルトのまま
![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/759a9e19-d91d-54a9-db44-5452670b5567.png)
　Training phrasesは最初にボットを呼びかけるような言葉を設定します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/b9f7b737-475f-f19e-c15c-3c6e4afd80ae.png)
　呼びかけられたら返す言葉を設定します。
　Default、Googleアシスタント、SLACK、LINEタブがあり、まずはDefaultタブで設定した言葉がチャットツール側に返答され、その後で各チャットツールのところで設定されたものが返答されます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/16227d74-0a9e-6938-ca43-d2f72102622b.png)
　LINEタブではQuick repliesで設定しているのでLINE側にボタン選択型メッセージが表示されます。

#### IntQuestionEJ
　Default Welcome Intentで１が選択されると呼び出されるインテントです。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/b2b19cc8-3add-6981-ccf7-64d3fdeedf3b.png)
　InputContextでMainMenuをセットしているのはDefault Welcome Intentから呼び出された場合にのみ有効ということを意味します。複数セットするとand条件になります。
　OutputContextのIntQuestionEJ-followupを設定し、このインテントが呼び出されていることを他のインテントに認識させます。MainMenuのライフスパンを０にしているのは、このコンテキストが残ることがあったので明示的に削除することを意味します。 
![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/5742f5a5-7154-dd3c-dd62-b8d9b831e87b.png)
　このイベントはプログラムからこのインテントへ遷移させるときに使用します。インテントのIDだと思えばわかりやすいかも。。。？
![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/75678f84-e821-08da-d6da-36c1e17102e4.png)
　トレーニングフレーズはこんな感じにしてあります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/a168e97d-d617-ec09-177d-90436a80acf1.png)
　レスポンスでは単語をランダムに返すように設定しています。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/2e033bc0-a7e2-87ad-9143-261868462a70.png)
　FulfillmentをONにしています。
　ここで気づいた方もいるかもしれませんが問題はプログラムで出力するはずなのにここでレスポンスを設定しています。
　その理由は２つあります。１つはコーディングを書く前段階でイメージをつかむためにです。もう１つはプログラムでレスポンスを返さずに終了した場合、このレスポンスが使用されます。まあ、できた後では削除してしまってもいいのですが、削除するのも面倒ですし、また使用する機会が生まれるかもしれないので残しています。
#### IntAnswerEJ
![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/c6da00f5-3218-1909-5975-72024c9ec830.png)
　タイトルのところがON/OFF設定できます。これをONにしている＝fallback intentということになります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/90210bc0-58a8-3b96-d90d-d601b30c097d.png)
　そして、InputContextにIntQuestionEJ-followupがセットされているので、IntQuestionEJが呼ばれた後に何かしら文字を入力された場合に呼び出されるということになります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/29d12e71-dfb8-ccc0-6e60-5384275ef10d.png)
　FullfillmentがONになっているので、このインテントが呼び出されると直ちにWebhookプログラムが呼び出されることになります。
#### IntAnswerEJ_correct
　このインテントは正解時にWebhookプログラムから呼び出されます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/4937a0c0-2a63-f443-025e-1f749a1d6b67.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/770b2ea1-343c-b471-5ae3-85317ff4a3ff.png)
　パラメータにはプログラムから送られた正解値を受け取れるようにする為に用意しています。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/828ad7f5-9858-41d7-15f6-458c275c48bb.png)
　レスポンスには正解のメッセージをランダムで返答するようにしています。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/f7cec5a1-021b-8cc5-ab93-66da1b1dac2e.png)
　LINEタブの設定で選択型メッセージを返すようにしています。
#### IntAnswerEJ_miss
　このインテントは不正解時にWebhookプログラムから呼び出されます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/fc64e142-0cd3-3428-af97-cdab70e6bd56.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/27a3a253-b285-7228-63e0-792fa6f1a98a.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/625e9b95-f26e-3023-5731-af1e83e8a86a.png)
　正解時のインテントとは逆のメッセージが表示されるようにしています。ここでセットしている。「正解：$correct」の部分がプログラムから受け取ったパラメータ値を出力しています。

#### Default Fallback Intent
#### IntQuestionJE
#### IntAnswerEJ - claim
#### IntAnswerEJ - end
#### IntAnswerEJ - next
#### IntBadWord
#### IntEnd

## Webhook(Firebase, Node.js)

[Dialogflowだけでオリジナルの3択クイズを作ろう](https://qiita.com/akikushi/items/e7de6bf7d47d12563488)
を参考に作らせていただきました。

```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, Suggestions, LinkOutSuggestions} = require('dialogflow-fulfillment');
 
process.env.DEBUG = 'dialogflow:debug'; // enables lib debugging statements

const quesion_ej = [
    { question : "action\n[ˈækʃən]", one : "行動", two : "事柄", three : "機関", correct : "行動"},
    { question : "weather\n[wéðɚ]", one : "風上", two : "天気", three : "勇気", correct : "天気"},
    { question : "athlete\n[ˈæθliːt]", one : "動き", two : "動作", three : "運動選手", correct : "運動選手"},
    { question : "barbeque\n[bɑ́rbɪkjù]", one : "焼き鳥", two : "バーベキュー", three : "焼肉", correct : "バーベキュー"},
    { question : "birthday\n[bˈɚːθdèɪ]", one : "誕生", two : "誕生日", three : "生まれる", correct : "誕生日"},
    { question : "business\n[bíznəs]", one : "トレンド", two : "職業", three : "仕える", correct : "職業"},
    { question : "clothes\n[klóʊ(ð)z]", one : "水着", two : "服", three : "靴", correct : "服"},
    { question : "February\n[fébruèri]", one : "３月", two : "１０月", three : "２月", correct : "２月"},
    { question : "hierarchy\n[hάɪ(ə)rὰɚki]", one : "階層", two : "規律", three : "法律", correct : "階層"}
];

exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
  const agent = new WebhookClient({ request, response });
  console.log('Dialogflow Request headers: ' + JSON.stringify(request.headers));
  console.log('Dialogflow Request body: ' + JSON.stringify(request.body));
 
  function welcome(agent) {
    agent.add(`Welcome to my agent!`);
  }
 
  function fallback(agent) {
    agent.add(`I didn't understand`);
    agent.add(`I'm sorry, can you try again?`);
  }

  function answer_ej_next(agent) {
    agent.setFollowupEvent('StartQuestionEJ');
  }

  function answer_ej_end(agent) {
    agent.setFollowupEvent('Welcome');
  }

  function question_ej(agent) {
    console.log("question_ej");

    var random = Math.floor( Math.random() * 9 );

    let myQuestionEJ = quesion_ej[random];

    // 質問を出力
    agent.add(myQuestionEJ.question);
//    agent.add(new Suggestion({title : 'どれにするかの？'}));
    agent.add(new Suggestion(myQuestionEJ.one));
    agent.add(new Suggestion(myQuestionEJ.two));
    agent.add(new Suggestion(myQuestionEJ.three));

    //答えを格納
    agent.setContext({ name: 'question_ej_answer', lifespan: 1, parameters: { correct: myQuestionEJ.correct}});

  }

  function answer_ej(agent){
    console.log("answer_ej");

    // 回答を取得
    var answer1 = request.body.queryResult.queryText;
    console.log("answer1 : " + answer1);

    // 正解を取得
    var context = agent.getContext('question_ej_answer');
    console.log("context : " +JSON.stringify(context));

    var correct = context.parameters.correct;
    console.log("correct : " + correct);

    var eventName = '';
    if(answer1 == correct){
        eventName = 'answer_ej_correct';
    } else {
        eventName = 'answer_ej_miss';
    }
    
    // 答えをeventのパラメータで返す
    let callEvent = {
      name: eventName,
      parameters: {correct: correct},
      languageCode: 'ja',
    };
    agent.setFollowupEvent(callEvent);
  }
  
  function googleAssistantHandler(agent) {
    let conv = agent.conv(); // Get Actions on Google library conv instance
    //agent.requestSource = agent.ACTIONS_ON_GOOGLE;
    //conv.close('Hello from the Actions on Google client library!'); // Use Actions on Google library
    conv.ask('Hello from the Actions on Google client library!'); // Use Actions on Google library
    agent.add(conv); // Add Actions on Google library responses to your agent's response
  }

  // Run the proper function handler based on the matched Dialogflow intent name
  let intentMap = new Map();
  intentMap.set('Default Welcome Intent', welcome);
  intentMap.set('Default Fallback Intent', fallback);
  intentMap.set('IntQuestionEJ', question_ej);
  intentMap.set('IntAnswerEJ', answer_ej);
  intentMap.set('IntAnswerEJ - next', answer_ej_next);
  intentMap.set('IntAnswerEJ - end', answer_ej_end);
  agent.handleRequest(intentMap);
});
```

## Actions on googleとの連携

## LINEとの連携
こちらを参考にさせていただきました。
[Dialogflowと連携してLINE Botを作る](https://qiita.com/poruruba/items/09563e251adef1e1e8af)
[DialogflowでLINE Botの応答をWebhookで成形してみた](https://qiita.com/h-takauma/items/77920c5088593a9bd25d)

## Slackとの連携
こちらを参考にさせていただきました。
[DialogflowからSlackのBotとして動かす](https://qiita.com/sanoh/items/393bfecb251035db7389)

## Facebook messengerとの連携

こちらの記事を参考にさせていただきました。
[DialogflowからFacebook MessengeのBotとして動かす](https://qiita.com/sanoh/items/11cefc5d31815f9ada74)

**Facebookページを作成**
FacebookのTOPページ→Facebookページ→Facebookページを作成ボタンをクリック

![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/6253fe42-a0ae-2079-5ebd-bfe6f7b28d67.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/b82c591f-7c75-db78-b233-c3eb872fd1c2.png)
あとはとりあえずスキップ

**次に作成したFacebookページと連携させるアプリを登録します**

DialogFlowでCallbackURLを生成
![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/25d3a93a-6cd0-5167-e665-b61c290b644e.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/243092ff-acf4-534b-ea59-4a0afa35bdcc.png)


Facebook Developer Consoleへ移動
https://developers.facebook.com/

マイアプリから新しいアプリを追加する
![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/403bee7a-506e-54a0-d410-2b0a6fc0e254.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/80e771f4-8bfc-c212-978a-61385167c987.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/7a40ab9a-be7c-6fb0-e026-67981cadd1d0.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/525ef44d-6149-1d90-a921-c9dbb80893bb.png)

Messengerの設定ボタンをクリック

![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/98099e69-8e13-0eb3-a836-ea2e9a4b3748.png)
ウィザードに従って進みます

![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/c5be9f63-2ec9-a462-9fc7-61b4ec2b07f1.png)
その後でアクセストークンが生成されます

![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/1d928c3b-b80a-692d-b7ef-7cacc3d7f409.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/cd214d1f-213d-11eb-8f4f-a191ffe7f94e.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/079f15d2-5739-6a5a-f569-3a762df066aa.png)

Facebook messengerに「うさ仙人」が表示されるのでメッセージを送信して返答があれば完了です。

# ハマったところ

**インテントとwebhook(google firebase)との連携**

**・インテントのトレーニングフレーズは誤検知されやすい**
　　たとえば、メニューで「１、２、３から選んでね」として、１２３だけを検知するインテントを作成。しかし、テストで「５月」と入力したのにこのインテントにヒットすることが判明。
**・アウトプットはインテントを使った方が便利**
　プログラムからメッセージを返すようにしようかと考えたが、どのように返すのかを調べるのに時間がかかった。知識がつくまではインテントを使った方がよさそう。

**・最初、Contextの使い方がよくわからなかったがフラグ変数と考えればスッキリした。**

**・FirebaseでDEPLOYした直後にテストをすると前のバージョンが動く**
![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/65177eb2-3ef7-fd7c-1464-ea2ce3fbf18b.png)
　ここでDEPLOYして完了していても、ちゃんと反映されるまでに数分かかるもよう。。。

# 分からなかったこと

　知っている方、コメントいただければ幸いです。

**・メッセージを返した後にインテントを移動する方法**
　インテントＡとＢがあり、
　Ａ→webhook→Ｂ→チェットメッセージ表示はできるが、
　Ａ→webhoo→メッセージ出力→Ｂとするとメッセージは出力されずにそのままＢへ移るだけ。

　addしたあとにeventをするとだめ
　
**・webhookプログラムを使わずにDialogFlowのインテントだけでインテント間を移動ができないか**

**・DialogFlowのインテントで条件設定できないか？**

**・FollowUpインテントを作成して、そのinput contextを削除して一旦セーブするとcontextを戻してもインテント一覧ではfollowupとならない。バグ？**

**・Googleアシスタントでsuggetion chipsを返すとcancelボタンが標準でついてくるがこれを消す方法**

**・webhookプログラムからsuggetion chipsを出力するとタイトルがつかない**
![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/fce029be-728f-8d79-d115-c3c3f0f12dad.png)
「choose an item」となる。

# その他参考文献
この本はオールカラーで見やすく、また内容も濃く、何度も読み返しました。おすすめです！

[【Google Home対応】ステップバイステップで力がつく Googleアシスタントアプリ開発入門](https://www.amazon.co.jp/%E3%80%90Google-Home%E5%AF%BE%E5%BF%9C%E3%80%91%E3%82%B9%E3%83%86%E3%83%83%E3%83%97%E3%83%90%E3%82%A4%E3%82%B9%E3%83%86%E3%83%83%E3%83%97%E3%81%A7%E5%8A%9B%E3%81%8C%E3%81%A4%E3%81%8F-Google%E3%82%A2%E3%82%B7%E3%82%B9%E3%82%BF%E3%83%B3%E3%83%88%E3%82%A2%E3%83%97%E3%83%AA%E9%96%8B%E7%99%BA%E5%85%A5%E9%96%80-%E9%87%8C%E5%B1%B1%E5%8D%97%E4%BA%BA-ebook/dp/B07HMYL12G/ref=tmm_kin_swatch_0?_encoding=UTF8&qid=1551966248&sr=8-1)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/115627/ae28fe2e-576e-4645-a6ce-34d35450ecdc.png)