PHP+Watson Assistant+楽天書籍検索APIでChatBotを作る！

この記事は [リンク情報システム](https://www.lis.co.jp) の「[Tech Connect Summer 夏のアドベントカレンダー](https://qiita.com/dgkz/items/2862d5e6b77203a126b3)」のリレー記事です。
engineer.hanzomon のグループメンバーによってリレーされます。

☆☆☆★クマさんチームの2日目です！★☆☆☆

@p3ngu1nさん、@n-tanimotoさんが以前のアドカレで楽天の書籍検索APIを使って面白そうなことをしていたので、流れに乗ってみました。PHP周りは 松浦健一郎/司ゆき『[おもしろまじめなチャットボットをつくろう](https://www.amazon.co.jp/%E3%81%8A%E3%82%82%E3%81%97%E3%82%8D%E3%81%BE%E3%81%98%E3%82%81%E3%81%AA%E3%83%81%E3%83%A3%E3%83%83%E3%83%88%E3%83%9C%E3%83%83%E3%83%88%E3%82%92%E3%81%A4%E3%81%8F%E3%82%8D%E3%81%86-%E6%9D%BE%E6%B5%A6%E5%81%A5%E4%B8%80%E9%83%8E-ebook/dp/B076J1V873)』を参考に作成しています。ChatBotを作るだけではなく様々なAPIも紹介しているので、是非お勧めします。

# 1.こんなの作ってみた
そこそこ自由な会話ができて、質問文がある程度ばらついても書籍の最新刊の発売日を応答できるようなChatBotを作ってみました。
![キャプチャ19.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327372/896f0a39-4632-eb8c-8641-bd7b45ea1f67.png)
# 2.ChatBotの概要
発言の解釈、応答文の選択等の「会話」はWatson Assistantサービスに任せます。LINEサーバからのリクエストを受け、Watson Assitant API・楽天APIを叩いてレスポンスを生成するAPサーバ部分が主に構築対象となります。
![キャプチャ2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327372/0a414aef-5005-a6be-c7f4-2f09c4dc4782.png)

#3.前準備
####1.サーバを準備する。
下記を準備します。
・SSL通信・PHPが利用可能であるサーバ
・FTPクライアント
※今回は[サクラレンタルサーバ](https://www.sakura.ne.jp/)(スタンダードプラン 月額515円)と[WinSCP](https://ja.osdn.net/projects/winscp/)、[Putty](https://putty.softonic.jp/)を利用しました。

####2.LINE Business IDに登録する。
[LINE Business ID](https://www.linebiz.com/jp/entry/)のアカウントを作成します。
個人のLINEアカウントで登録する場合は、Business ID登録前にLINEアプリを開いて[設定]＞[アカウント]でメールアドレスとパスワードを設定しておくとスムーズです。
※作成するBusiness IDアカウントは無料プラン・未認証アカウントで十分です。

####3.Rakuten Developpersに登録する。
[Rakuten Developpers](https://webservice.rakuten.co.jp/)のアカウントを作成します。
自作アプリからAPIを叩く場合に必要ですので、アプリID発行を発行しておきます。
※無料です。

####4.IBM Cloudに登録する。
[IBM Cloud](https://cloud.ibm.com/registration)のアカウントを作成します。
※こちらも無料枠で作成できます。

#4.ChatBotの設定をする
####1.LINE Developersでチャネルを作成する。
作成したLINE Business IDで[LINE Developers](https://developers.line.biz/ja/?status=success)にログインします。
トップ画面から[プロバイダー]＞[新規プロバイダー作成]を選択し、プロバイダーを作成します。
![キャプチャ4.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327372/847ca511-9c5a-af61-c415-83888376d543.png)

トップ画面で作成したプロバイダーを選択して、[新規チャネル作成]から「Message API」でチャネルを作成します。
![キャプチャ9.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327372/0d45676f-7a6d-1c21-2b72-5c7327cc3dd9.png)


作成した新規チャネルのアクセストークンを確認します。AP⇔LINEサーバとの疎通に使用します。
![キャプチャ6.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327372/458a55c2-c0f8-65e1-cb11-0b62ddde5ae8.png)

チャネルはQRコードからLINEアプリで友達登録可能です。初期設定だと自動応答がONになっており、コメントすると画像のようなことを言われます。
![キャプチャ7.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327372/7cec2bcb-cebc-5bc0-f61c-1b2e0c2f6d9e.png)
会話はWatson Assistantに任せるので[チャネル基本設定]＞[LINE@機能の利用]＞[自動応答メッセージ]で応答メッセージをOFFにしておきます。
####2.IBM CloudでWatson Assistantサービスを作成する。
[IBM Cloud](https://cloud.ibm.com/login)にログインします。
[ダッシュボード]>[サービス]>[リソースの作成]を選択します。
![キャプチャ10.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327372/8fcf5046-5177-70f6-d746-4f7c315ba014.png)
サービスはWatson Assistantを選択します。後は指示に従ってサービス名などを設定していきます。10,000メッセージ/月までは無料となっています。(2019年7月現在)
![キャプチャ11.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327372/e83da331-1924-344d-2f88-9870f16d8d10.png)
リソースリストから[サービス]＞[作成したWatson Assistantサービス]を選択し、API鍵とURLを確認します。AP⇔Watson Assistantの疎通に使います。
![キャプチャ12.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327372/98a2bb2a-647a-3172-79d8-66dffc4cbb1c.png)
[Watson Assistantの起動]を選択して、SkillとAssistantを作成します。
設定方法は長くなるので割愛しますが、[チュートリアル](https://cloud.ibm.com/docs/services/assistant?topic=assistant-getting-started#getting-started-tutorial)や[公式ドキュメント](https://www.ibm.com/developerworks/jp/cognitive/library/cc-watson-chatbot-conversation/index.html)が参考になります。

本ボットでは下画像のように、書籍検索Intentを作成しました。
登録した言葉と完全一致しなくても、Watson Assistantが良いように解釈してくれます。
![キャプチャ15.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327372/ee43d257-664b-3770-94a6-9df2104e19fe.png)

上記の設定から「本の発売日を質問している」とWatson Assistantが解釈した時、AP側で検索API等を叩かせたい場合は応答電文のContextにトリガーとして値を設定します。AP側は応答電文のContext値の有無で動作を変えるようにコーディングします。
![キャプチャ16.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327372/594bbef5-2b16-a49e-24b0-b92227ec022e.png)

Context値を設定した場合の応答電文は下記のようになります。

```json:応答電文
{
  "output":
    {
      "generic":[
        {
          "response_type":"text",
          "text":""
        }
      ],
      "intents":[
        {
          "intent":"書籍検索",
          "confidence":1
        }
      ],
      "entities":[
        {
          "entity":"書籍",
          "location":[0,5],
          "value":"乙嫁語り",
          "confidence":1
        }
      ]
    }
}
```
以上で、ChatBot側の設定は完了です。


#5.APサーバ側を作成
アプリ側は、下記の機能を実装します。
・LINEサーバからリクエストを受けて、メッセージをWatson Assitantに渡す。
・Watson Assistantのレスポンスを見て、楽天APIを実行する。
・Responseにメッセージを詰めてLINEサーバに応答する。
※PHPは今回初めていじったので色々手さぐりです汗

フォルダ構成は下記となっています。

```bash:ディレクトリ構成図
bot
├──main.php                   ---メイン関数
├── common
│   ├── func.php              --- 共通関数
│   └── var.php               --- 共通変数
├── log
│   └── debug_yyyyMMdd.txt    --- ログ出力
└── logic
    ├── exec_bot.php          --- WatsonAPI実行関数
    └── search_book.php       --- 楽天API実行関数
```
共通変数は下記の通りです。色々と各方面のAPIを叩くので共通変数化しておくと管理しやすいと思います。

```php:bot/common/var.php
<?php

// 実行日時
define('TODAY', date("Y/m/d"));
// ログファイルの指定
define('DEBUG', 'log/'.'debug_'.date("Ymd").'.txt');
// LINE URL
define('LINE_URL', 'https://api.line.me/v2/bot/message/');
// LINE アクセストークン
define('TOKEN', '**LINEのアクセスTokenを指定**');
// LINE管理ユーザー
define('DEBUG_USER', '**エラー発生時LINEに通知してほしいユーザを指定**');
// 楽天API URL
define('RAKUTEN_BOOK_URL', 'https://app.rakuten.co.jp/services/api/BooksBook/Search/20170404');
// 楽天API アプリID
define('RAKUTEN_ID', '**楽天Developpers登録時に発行した値を指定**');
// Watson Assistant URL
define('WATSON_ASS_JAPAN', 'https://gateway-tok.watsonplatform.net/assistant/api/v2/assistants/');
// Watson API version
define('WATSON_API_VER', '2019-02-28');
// Watson Assistant ID
define('WATSON_ASS_ID', '**Watson Assistant IDを指定**');
// Watson Assistant API鍵
define('WATSON_ASS_KEY', '**Watson Assistant keyを指定**');
```

共通関数は下記のように作成しました。
Watson Assistant-v2 APIを使用しているため、アクセスするには一旦セッションIDを生成する必要があります。(⇒[Watson Assitant API v2](https://cloud.ibm.com/apidocs/assistant-v2))
そのため、(7)Watson API session 生成と(8) Watson API session 破棄でセッションの生成→削除を行います。

```php:bot/common/func.php
<?php

// (1)ログ出力関数
function debug($title, $text) {
    // こんな感じで出します。2019/07/21 16:54:59[INFO]～
    file_put_contents(DEBUG, date("Y/m/d H:i:s").'['.$title.']'.$text."\n", FILE_APPEND);
}

// (2)LINEサーバへ送信実行関数
function post($url, $object) {
    // JSON形式への変換
    $json=json_encode($object);
    // ログ出力
    debug('INFO', $json);

    // 送信の準備
    $curl=curl_init(LINE_URL.$url);
    curl_setopt($curl, CURLOPT_POST, true);
    curl_setopt($curl, CURLOPT_POSTFIELDS, $json);
    curl_setopt($curl, CURLOPT_HTTPHEADER, [
        'Content-Type: application/json',
        'Authorization: Bearer '.TOKEN
    ]);

    // 送信の実行
    $result=curl_exec($curl);

    // 送信の終了
    curl_close($curl);
}

// (3)LINEサーバへの送信データ生成関数
function reply($event, $text) {
    // 送信のデータの作成
    $object=[
        'replyToken'=>$event->replyToken,
        'messages'=>[['type'=>'text', 'text'=>$text]]
    ];

    // 送信実行
    post('reply', $object);
}

// (4)LINEプッシュ関数
function push($to, $text) {
    // 送信データの作成
    $object=[
        'to'=>$to,
        'messages'=>[['type'=>'text', 'text'=>$text]]
    ];

    // 送信
    post('push', $object);
}

// (5)楽天API応答電文の読み込み
function load($file) {
    // 応答電文の読み込み
    $json=file_get_contents($file);

    // JSONからPHPへ変換
    return json_decode($json);
}

// (6)yyyy年MM月dd日をyyyy/MM/dd形式に変換
// 楽天APIのレスポンス成型用に使います。
function dateConv($str) {
    // 半角・全角数字以外の文字列を変換
    $strConv=mb_ereg_replace('[^0-9０-９]+', '/', $str);
    // 最後の1字を削除
    $strConv=substr($strConv, 0, -1);

    return $strConv;
}

// (7)Watson API session 生成
// assistant-v2 APIの場合は、sessionの生成が必要です。
function getSessionWt() {
    // urlの作成
    $url=WATSON_ASS_JAPAN.WATSON_ASS_ID.'/sessions?version='.WATSON_API_VER;
    debug('INFO', $url);

    // 送信の準備
    $curl=curl_init($url);
    curl_setopt($curl, CURLOPT_USERPWD, 'apikey:'.WATSON_ASS_KEY);
    curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);

    // 送信の実行
    $result=curl_exec($curl);
    debug('INFO', 'response:'.$result);

    // 送信の終了
    curl_close($curl);

    // JSONからPHPへ変換
    return json_decode($result);
}

// (8) Watson API session 破棄
// assistant-v2 APIで生成したsessionは時間で削除されますが、削除処理を行います。
function deleteSessionWt($sessionId) {
    // urlの作成
    $url=WATSON_ASS_JAPAN.WATSON_ASS_ID.'/sessions/'.$sessionId.'?version='.WATSON_API_VER;
    debug('INFO', $url);

    // 送信の準備
    $curl=curl_init($url);
    curl_setopt($curl, CURLOPT_USERPWD, 'apikey:'.WATSON_ASS_KEY);
    curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE');
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);

    // 送信の実行
    $result=curl_exec($curl);
    debug('INFO', 'response:'.$result);

    // 送信の終了
    curl_close($curl);

    // JSONからPHPへ変換
    return json_decode($result, true);
}

// (9) Watson API ユーザー入力を送信
function sendMessagenWt($sessionId,$text) {

    // 送信のデータの作成
    $object=[
        'input'=>['text'=>$text]
    ];

    // JSONへの変換
    $json=json_encode($object);
    debug('INFO', $json);

    // urlの作成
    $url=WATSON_ASS_JAPAN.WATSON_ASS_ID.'/sessions/'.$sessionId.'/message?version='.WATSON_API_VER;
    debug('INFO', $url);

    // 送信の準備
    $curl=curl_init($url);
    curl_setopt($curl, CURLOPT_USERPWD, 'apikey:'.WATSON_ASS_KEY);
    curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($curl, CURLOPT_POSTFIELDS, $json);
    curl_setopt($curl, CURLOPT_HTTPHEADER, [
        'Content-Type: application/json'
    ]);

    // 送信の実行
    $result=curl_exec($curl);

    // 送信の終了
    curl_close($curl);

    // JSONからPHPへ変換
    return json_decode($result, true);
}

```

次はWatson API実行関数です。本関数で、Watson API session 生成関数を呼び出し、sessionIdとユーザーのメッセージをWatson Assistantに渡します。WatsonのResponseにContextが含まれていた場合（=本の発売日についての質問だった時）、楽天APIを実行します。

```php:bot/logic/exec_bot.php
<?php

function bot($event) {

    // Watson session idを取得
    $session=getSessionWt();
    $sessionId=$session->session_id;

    // session idを取得できなかった場合
    if (empty($sessionId)) {
        throw new Exception('Could not get session id!');
    }

    // ユーザー入力を取得
    $text=$event->message->text;

    // ユーザー入力をassistant-v2 APIに送信
    $response=sendMessagenWt($sessionId,$text);

    // データ数を取得
    // Watson Assistantから応答されたメッセージ
    $json_text=count($response["output"]["generic"]);
    // Watson AssistantがContextに詰めた情報
    $json_entity=count($response["output"]["entities"]);

    // 応答メッセージを詰める
    for($i=$json_text-1;$i>=0;$i--){
        $returnMsg.=$response["output"]["generic"][$i]["text"];
    }
    // Contextを詰める
    for ($i=$json_text-1;$i>=0;$i--){
        $returnEntity.=$response["output"]["entities"][$i]["value"];
    }

    // 楽天API使用ケースか判断
    if (!empty($returnEntity)) {
        // 書籍検索APIを実行
        search_book($event, $returnEntity);
    } else {
        // LINEサーバへ応答
        reply($event, $returnMsg);
    }

    // Watsonセッションを破棄
    $result=deleteSessionWt($sessionId);

    // 正常に破棄されなかった場合
    if (!empty($result)) {
        // Exceptionを投げる
        throw new Exception('Failed to delete Watson session:'.$sessionId);
    }

}
```


楽天API実行関数は下記の通りです。最新刊の発売日を取得するように、リリース日が早い順にソートをかけるようにリクエストしています。発売日によって、メッセージを変えるように分岐させました。

```php:bot/logic/search_book.php
<?php

function search_book($event, $title) {

    // URLの作成
    $url=RAKUTEN_BOOK_URL.'?applicationId='.RAKUTEN_ID;
    $url.='&title='.urlencode($title);
    $url.='&sort='.urlencode('-releaseDate');
    $url.='&hits=1';
    debug('url', $url);

    // 商品検索の実行
    $result=load($url);

    // 検索結果の表示
    if (!empty($result)) {
        $text='「'.$title."」最新刊の発売日は";
        // 検索結果が0でない場合
        if (strcmp("$result->count", "0") != 0) {
            // Itemsは配列型で返ってくるのでループします。
            foreach ($result->Items as $item) {
                $salesDate=$item->Item->salesDate;
                // 発売日フォーマット変更
                $salesConvDate=dateConv($salesDate);

                if ($salesConvDate > TODAY) {
                    $text.=$salesDate."です！";
                } elseif($salesConvDate == TODAY) {
                    $text.="本日".$saledDate."です！！！";
                } else {
                    $text.=$salesDate."でした！\n";
                    $text.="次巻の予定は未定です。\n";
                }

                // Jsonから対象のURLを取得
                $text.=$item->Item->itemUrl;
            }
        } else {
            $text.="見つかりませんでした。";
        }
    } else {
        // 楽天APIが何も返さなかった場合
        // Exceptionを投げる
        throw new Exception('Failed to connect Rakuten API');
    }
    // LINEサーバへ応答
    reply($event, $text);
}
```
最後にmain関数です。LINEサーバにはこのファイルにアクセスさせ、ここでリクエストを受けます(URLはhttps\://XXXXXX/bot/main.phpとなります。)
エラー発生時はログ出力して、共通変数に設定したLINE IDに通知が行くようにpush機能をつけました。

```php:bot/main.php
<?php

//  共通ファイル読み込み
require_once('common/func.php');
require_once('common/var.php');
require_once('logic/search_book.php');
require_once('logic/exec_bot.php');

// リクエストの取得
$input=file_get_contents('php://input');
debug('INFO', $input);

// リクエストが空でないことを確認
if (!empty($input)) {

    // イベントの取得
    $events=json_decode($input)->events;

    // 各イベントに対するBotプログラムの実行
    foreach ($events as $event) {
        try {
            // botの実行
            bot($event);
        } catch (Exception $e) {
            // エラーMsg作成
            $errMsg="ERROR:".$e->getMessage();

            // ログ出力
            debug('ERROR', $errMsg);

            // LINE管理アカウントに通知
            push(DEBUG_USER, $errMsg);
        }
    }
}
```

APサーバにFTPクライアントでプログラムを格納したら、[LINE Developers](https://developers.line.biz/ja/?status=success)にログインし、[プロバイダーリスト]＞[作成したプロバイダー]＞[チャネル]＞[チャネル基本設定]で、Webhook URLにAPサーバのURLを追加します。接続確認を選択して、正しく接続できることを確認します。
![キャプチャ17.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327372/9221ae51-b0e9-1f32-6bf1-22453760bf36.png)

LINEアプリでメッセージを投げてChatBotから応答があればOKです。
![キャプチャ21.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327372/c8f7ccfd-2455-c1b0-be12-82049c411484.png)
:relaxed::sunny:
※応答ない場合は、まずphpが書かれたファイルの文字コードがUTF-8であるか確認してください。

#6.まとめ

今回のChatBot作成で使った技術の大半は初めて触れたものでした。出来はともかく、業務と違う技術に触れられてとても楽しかったです。Watson Assistantの調整が難しかったです。良い教師あってのAIだと感じました。

クマさんチームの3日目は@h-yamasakiさんです！！お楽しみに！！

---
[リンク情報システム株式会社](https://www.lis.co.jp)では一緒に働く仲間を[随時募集](https://www.lis.co.jp/employ_top)しています！
また、お仕事のご依頼(チャットボットのご用命等)、ビジネスパートナー様も募集しております。[お気軽にご連絡](https://www.lis.co.jp/inquiry)ください。