【LINE Bot】Heroku+PHP+雑談対話APIで会話Bot作ってみた

LINE Botと雑談APIを使って、会話Botを作ってみました。

##雑談対話APIとは
docomoさんが提供している自然な会話・雑談を実現するためのAPIです。
ユーザが入力した発言に対して、APIから返事がレスポンスされます。
https://dev.smt.docomo.ne.jp/?p=docs.api.page&api_name=dialogue&p_name=api_usage_scenario

>ユーザの何気ない一言にバリエーション豊富な応答を返す、コンピュータと雑談を楽しむことができるAPIです。雑談対話APIはユーザのどんな発話に対しても必ず応答します。

##docomo Developer supportへの登録
雑談対話APIを使用するために、開発者登録を行います。

- docomo Developer support へようこそ  
https://dev.smt.docomo.ne.jp/?p=login

登録完了後、APIの利用申請を行いますが、申請後即使用可能になりました。とても便利:clap: 
（ちなみに法人情報の入力を勧められますが、入力しなくても問題なく使えました。APIリクエスト数が個人ユーズの範囲を越える（Botを一般公開する、など）する時は、入力したほうが良いのかも・・・と思っています。）

##Heroku環境のセットアップ
先にBotを動かすインフラをセットアップします。今回はHerokuにPHPの環境を作りました。

- PHP on Heroku  
https://devcenter.heroku.com/categories/php

アプリを作成したら、「Fixie」というアドオンをインストールします。
`$ heroku addons:create fixie:tricycle`

- Fixie  
https://elements.heroku.com/addons/fixie

>Static IP addresses for outbound requests

##BOT API Trial Accountへの登録
LINE Bot APIの開発者登録をします。先着10,000名とのことですが、4/11時点では問題なく登録可能でした。
https://business.line.me/ja/products/4/introduction

特に審査期間もなく、登録後はすぐにAPIを使用することができます。

##BOT APIの設定
###Basic information
`callback URL`は、`https://(HerokuアプリのURL):443/callback`を設定します。  
<img width="453" alt="スクリーンショット 2016-04-13 21.29.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/38442/e7cb9c2a-bfa6-7366-2aa9-131a6f8e9290.png">

###Server IP Whitelist
先ほどセットアップしたHerokuアプリに戻ります。ダッシュボードからFixieをクリック。  
<img width="993" alt="スクリーンショット 2016-04-13 21.45.17.png" src="https://qiita-image-store.s3.amazonaws.com/0/38442/15a9c7f2-b045-9575-73cd-34a645efd222.png">

Account->AccountDetailを表示します。
<img width="812" alt="スクリーンショット 2016-04-13 21.45.46.png" src="https://qiita-image-store.s3.amazonaws.com/0/38442/f82bb2a0-17bc-b657-ab4e-d7c87976f381.png">

Outbound IPsの値を、LINE BotのServer IP Whitelistに転記します。
<img width="410" alt="スクリーンショット 2016-04-13 21.55.14.png" src="https://qiita-image-store.s3.amazonaws.com/0/38442/d771916e-8540-5dce-0941-9bb7fe0c27e7.png">

##環境変数の設定
Herokuアプリに戻って、LINE Botのパラメータを設定します。
<img width="979" alt="スクリーンショット 2016-04-13 22.05.45.png" src="https://qiita-image-store.s3.amazonaws.com/0/38442/2eadc0ea-ce7a-a99f-cfa0-a519b3912f9e.png">
<img width="409" alt="スクリーンショット 2016-04-13 22.08.46.png" src="https://qiita-image-store.s3.amazonaws.com/0/38442/98365f52-ecaf-8f77-eebb-b5271d0558dc.png">

##ソースコード
```php:index.php
<?php
require_once __DIR__ . '/vendor/autoload.php';
use Symfony\Component\HttpFoundation\Request;

$app = new Silex\Application();
$app->post('/callback', function (Request $request) use ($app) {
    $client = new GuzzleHttp\Client();
    $body = json_decode($request->getContent(), true);
    foreach ($body['result'] as $msg) {
        $reply_message = chat($msg['content']['text']);

        $resContent = $msg['content'];
        $resContent['text'] = $reply_message;
        $requestOptions = [
            'body' => json_encode([
                'to' => [$msg['content']['from']],
                'toChannel' => 1383378250, #Fixed value
                'eventType' => '138311608800106203', #Fixed value
                'content' => $resContent,
            ]),
            'headers' => [
                'Content-Type' => 'application/json; charset=UTF-8',
                'X-Line-ChannelID' => getenv('LINE_CHANNEL_ID'),
                'X-Line-ChannelSecret' => getenv('LINE_CHANNEL_SECRET'),
                'X-Line-Trusted-User-With-ACL' => getenv('LINE_CHANNEL_MID'),
            ],
            'proxy' => [
                'https' => getenv('FIXIE_URL'),
            ],
        ];
        try {
            $client->request('post', 'https://trialbot-api.line.me/v1/events', $requestOptions);
        } catch (Exception $e) {
            error_log($e->getMessage());
        }
    }
    return 'OK';
});

$app->run();

function chat($send_message) {
    // docomo chatAPI
    $api_key = 'your docomoAPI key';
    $api_url = sprintf('https://api.apigw.smt.docomo.ne.jp/dialogue/v1/dialogue?APIKEY=%s', $api_key);
    $req_body = array('utt' => $text);
    $req_body['context'] = $send_message;

    $headers = array(
        'Content-Type: application/json; charset=UTF-8',
    );
    $options = array(
        'http'=>array(
            'method'  => 'POST',
            'header'  => implode("\r\n", $headers),
            'content' => json_encode($req_body),
            )
        );
    $stream = stream_context_create($options);
    $res = json_decode(file_get_contents($api_url, false, $stream));

    return $res->utt;
}
```

##動作結果
<img width="452" alt="スクリーンショット 2016-04-13 22.21.24.png" src="https://qiita-image-store.s3.amazonaws.com/0/38442/aa382f0e-0965-2e3d-f945-13604186d42e.png">
＿人人人人人人人人人＿
＞　会話にならない　＜
￣Y^Y^Y^Y^Y^Y^Y^Y￣

##実装修正（2016.05.10追記）
2回目以降の会話には`context`をリクエスト指定しないといけないとご指摘頂きました。:bow:
雑談会話APIからレスポンスされる`context`をRedisに保存して使用します。
（参考記事: LINE Botとおしゃべりする。http://qiita.com/tkitauji/items/0e1d25bc55c7ea6dcfed ）

```php:index.php
<?php
require_once __DIR__ . '/vendor/autoload.php';
use Symfony\Component\HttpFoundation\Request;

$app = new Silex\Application();
$app->post('/callback', function (Request $request) use ($app) {
    $client = new GuzzleHttp\Client();
    $body = json_decode($request->getContent(), true);
    foreach ($body['result'] as $msg) {
        // get from and message
        $from = $msg['content']['from'];
        $message = $msg['content']['text'];
        // get context from Redis
        $redis = new Predis\Client(getenv('REDISTOGO_URL'));
        $context = $redis->get($from);
        // chat API
        $response = chat($message, $context);
        // save context to Redis
        $redis->set($from, $response->context);

        $res_content = $msg['content'];
        $res_content['text'] = $response;
        $requestOptions = [
            'body' => json_encode([
                'to' => [$from],
                'toChannel' => 1383378250, #Fixed value
                'eventType' => '138311608800106203', #Fixed value
                "content" => $res_content,
            ]),
            'headers' => [
                'Content-Type' => 'application/json; charset=UTF-8',
                'X-Line-ChannelID' => getenv('LINE_CHANNEL_ID'),
                'X-Line-ChannelSecret' => getenv('LINE_CHANNEL_SECRET'),
                'X-Line-Trusted-User-With-ACL' => getenv('LINE_CHANNEL_MID'),
            ],
            'proxy' => [
                'https' => getenv('FIXIE_URL'),
            ],
        ];
        try {
            $client->request('post', 'https://trialbot-api.line.me/v1/events', $requestOptions);
        } catch (Exception $e) {
            error_log($e->getMessage());
        }
    }
    return 'OK';
});

$app->run();

function chat($message, $context) {
    $api_key = 'your docomoAPI key';
    $api_url = sprintf('https://api.apigw.smt.docomo.ne.jp/dialogue/v1/dialogue?APIKEY=%s', $api_key);
    $req_body = array(
        'utt' => $message,
        'context' => $context,
    );
    $req_body['context'] = $message;

    $headers = array(
        'Content-Type: application/json; charset=UTF-8',
    );
    $options = array(
        'http'=>array(
            'method'  => 'POST',
            'header'  => implode("\r\n", $headers),
            'content' => json_encode($req_body),
            )
        );
    $stream = stream_context_create($options);
    $res = json_decode(file_get_contents($api_url, false, $stream));

    return $res->utt;
}
```

<img width="452" alt="スクリーンショット 2016-05-10 14.42.48.png" src="https://qiita-image-store.s3.amazonaws.com/0/38442/2e4ab8dc-c06b-8f74-c081-b02341b3bdb4.png">

会話できました！！

<img width="349" alt="スクリーンショット 2016-05-10 14.44.03.png" src="https://qiita-image-store.s3.amazonaws.com/0/38442/dee9423c-66d4-0cc3-dbaa-3fb480cd9887.png">

結構たのしい。

##その他改善策
- 雑談対話(認証あり)を使う  
https://dev.smt.docomo.ne.jp/?p=docs.api.page&api_name=dialogue&p_name=api_2#tag01  
アカウント認証をすることで、より精度の高い対話をすることができるようです。

>雑談対話APIにdアカウントによるユーザ認証・許可を実施することで、よりユーザの趣味嗜好に基づいた応答が可能となります。

- シナリオ対話APIを使用する  
https://dev.smt.docomo.ne.jp/?p=docs.api.page&api_name=scenario_dialogue&p_name=api_usage_scenario  
あらかじめ用意したシナリオに沿って対話ができるようです。（パターン考えるの大変そう・・・）

##Fixieについて
tricycleプランだと、500req/月まで無料のようです。
たくさん会話したい時は課金しないとですね...:innocent: 
<img width="479" alt="スクリーンショット 2016-04-14 15.42.30.png" src="https://qiita-image-store.s3.amazonaws.com/0/38442/3266277c-9675-0eff-46e4-027c7512dd5b.png">

##まとめ
今回は半分ネタで会話Botを作ってみましたが、LINE BotとREST APIの相性は抜群で、アイディア次第でいろんなことができそうだなーと手応えを感じました。

##参考URL
先人のみなさま、大変参考になりました。ありがとうございました。。！:pray: 

- LINE BOT をとりあえずタダで Heroku で動かす  
http://qiita.com/yuya_takeyama/items/0660a59d13e2cd0b2516
- PHP+HerokuでLINE BOT作ってみた  
http://qiita.com/qckanemoto/items/7c148fcc595cec4aa59a
- docomo 雑談対話 API をさくっと php で使う  
https://gist.github.com/wokamoto/c0195e0edc9abbac584b#file-docomo_dialogue_api-php