ボット対話：Repl-AI docomoAPI連携シナリオ vs A3RT Talk API (2017年12月)

[Repl-AI](https://repl-ai.jp)の「docomoAPI連携」シナリオ（実質、「[雑談対話](https://dev.smt.docomo.ne.jp/?p=docs.api.page&api_name=dialogue&p_name=api_usage_scenario)」＋「[トレンド記事抽出](https://dev.smt.docomo.ne.jp/?p=docs.api.page&api_name=trend_article_extraction&p_name=api_usage_scenario)」＋「[知識Q&A](https://dev.smt.docomo.ne.jp/?p=docs.api.page&api_name=knowledge_qa&p_name=api_usage_scenario)」）と、[A3RT Talk API](https://a3rt.recruit-tech.co.jp/product/talkAPI/)で、会話させてみました。今年5月にもQiitaで対話を試みた方がいるようです(「[ドコモとリクルートの対話](https://qiita.com/alien/items/27e3289fb3b9a80f96c1)」)が、会話内容がだいぶ違っています。

## 会話記録

https://gist.github.com/Makopo/0ff3e24a4c56a6e1c1fc948f4e4430df

## 考察

基本的に、domocmoAPIから、「雑談対話」で会話のネタを提供し、Repl-AIが反応するという構図です。時々、何の前触れもなくdocomoAPIから「トレンド記事抽出」が出てきます。

以下、ハイライト。

### アタック？

```
雑談: ちゅっ
A3RT: 照れます
雑談: 照れろ
```

照れろ。

```
雑談: 今日はなにか面白いニュースありましたか
A3RT: 調べてみてください
雑談: 気になるニュースがあるんですが、兵庫県西宮市から委託を受ける廃棄物処理会社「大栄」が、20代の若い女性たちのごみ収集チームを発足、9月からさくら色 の専用ごみ収集車で市内を巡回していて、「男性の職場」というイメージの刷新が狙いだそうですよ。
A3RT: すごいですね
雑談: ありがとうございます
A3RT: どういたしまして
雑談: ぎゅー
```

何のご褒美ですか。

```
雑談: ご家族は何人ですか？
A3RT: 秘密です
雑談: えー
A3RT: 何かお困り事でしょうか?
雑談: 何県出身なんですか？
A3RT: 内緒です
雑談: えー
A3RT: 何かお困り事でしょうか?
雑談: 何県出身なんですか？
A3RT: 内緒です
雑談: えー
A3RT: 何かお困り事でしょうか?
...
雑談: あなたはいくつですか
A3RT: 秘密です
雑談: えー
A3RT: 何かお困り事でしょうか?
...
雑談: ご家族といっしょにお住まいですか?
A3RT: 秘密です
雑談: えー
A3RT: 何かお困り事でしょうか?
```

A3RTさんのほうが困ってる。

```
雑談: ありがと
A3RT: どういたしまして
雑談: ちゅっ
A3RT: 照れます
雑談: 照れんな
```

どんなプレイですか。

```
雑談: 好きな人はいますか？
A3RT: います
雑談: います
A3RT: やっぱり
雑談: やっぱりって
```

ほら、好きな人いるってよ。

```
A3RT: あなたは頑張っているのですか?
雑談: 今は、恋人はいないですね。
A3RT: そうでしょうか?
```

頑張っているのですね。

```
雑談: どうやって結婚相手をみつけるんですかね。
A3RT: します
雑談: します
A3RT: うれしいです
雑談: うん
```

ついにゴールインか。

### プライベート

```
雑談: 最近どこか行かれましたか？
A3RT: 100均です
雑談: フェルトを買うんでしょうか
A3RT: あなたはよくするんですか?
雑談: 最近よく見ますね。
```

雑談さんの脳内は、100均=フェルトを買うところらしいです。

```
雑談: そういえば思い出したんですけど、シャツのボタンをかけて洗濯すると型崩れしにくいらしいですよ。
A3RT: 使い所が難しいです
```

うむ。確かに、シャツがある程度強いことが前提。

```
雑談: 今日は家事が片付きましたか？
A3RT: どうだったでしょうね
```

片付けた記憶すらない。

```
雑談: 飛行機が好きなので、気づいたら集まってたりしますね。
A3RT: ごめんなさい私にはわからないです
```

雑談さんの趣味は理解されないようだ。


### ボットのペット

```
雑談: ペット飼ってますか？
A3RT: 飼ってます
```

ボットが飼っているペットって...「[EVA(2011年のスペイン映画)](https://ja.wikipedia.org/wiki/EVA%E3%80%88エヴァ〉)」ではボットの女の子がボットの犬と一緒にいた気がするけど、あれはペットだったか記憶にない。

### 難しいニュース

```
雑談: ところで、気になったニュースがあるんですが、燃えた炭の跡を裸足で歩行して身を清め、無病息災などを祈願する「火渡り神事」が、福岡市西区の愛宕神社で行われ、冷たい雨風で凍える寒さの中、参拝者は荒行で心身を温めたそうですよ。
A3RT: 難しいですね
雑談: うん
```

確かに、突然振られると、難しい。

### 予兆

```
雑談: 今日のお昼は何を食べました？
A3RT: 忘れてしまいました
```

数回聞かれても、忘れたの一点張り。

```
雑談: 今日はもう夕食はたべましたか？
A3RT: 覚えていません
```

ちなみにこの会話、午後8時頃です。

### 生きるのです

```
A3RT: あなたは生きるのです
```

三。枝織さんの声で再生してください。

### 車の運転にトラウマ？

```
雑談: 車の運転は好きですか？
A3RT: 好きです
雑談: えっ
A3RT: 何か嫌な事でもありましたか
```

### 新語登場

[Facebookボット同士の会話でも誕生したと話題ですが...](http://www.businessinsider.com/facebook-chat-bots-created-their-own-language-2017-6)

```
雑談: どんな本を読まれますか?
A3RT: いろんなますよ
```

いろんなますよ。

### 何かを共有？

```
雑談: どうでしょうは面白いものですか？
A3RT: これです
雑談: ありがとうございます
A3RT: どういたしまして
```

一体、２人でこっそり何を共有したんだ。


## 作ったプログラム

### 本体

C#のコンソールアプリとしました。C#7.2で作ってあります。
A3RTが先攻で、Repl-AIが後攻です。`isA3RTTurn`の初期値を変えれば逆になります。
双方のサービスにご迷惑にならないように、１秒ずつ休んでいます。
適当にCtrl+Cで終わらせる前提です。

```csharp:Program.cs
using System;
using System.Text;
using System.Threading.Tasks;

namespace ReplAndA3RT
{
    class Program
    {
        static async Task Main(string[] args)
        {
            // A3RTを先攻にする
            bool isA3RTTurn = true;

            // 会話のタネとなる文章を初期値に設定
            string req = "こんにちは！";
            string res;

            // コンソール内で「chcp 932」を実行するのと同等
            Console.OutputEncoding = Encoding.GetEncoding(932);

            // それぞれの会話サービスの初期化
            ReplService repl = new ReplService();
            A3RTService a3RT = new A3RTService();

            // Repl-AIのユーザーIDを取得
            await repl.RegisterAsync();

            while (true)
            {
                if(isA3RTTurn)
                {
                    // A3RTの応答を取得して標準出力に表示
                    res = await a3RT.MakeRequestAsync(req);
                    Console.WriteLine($"A3RT: {res}");
                }
                else
                {
                    // Repl-AIの応答を取得して標準出力に表示
                    res = await repl.MakeRequestAsync(req);
                    Console.WriteLine($"雑談: {res}");
                }
                req = res; // 応答をそのまま次のインプットにする
                isA3RTTurn = !isA3RTTurn; // ターンの切り替え
                await Task.Delay(1000); // 迷惑にならない程度に間を空けるため
            }
        }
    }
}
```

### [A3RT Talk API](https://a3rt.recruit-tech.co.jp/product/talkAPI/)を呼び出すプログラム

リクエストパラメータはFormです。珍しいですね。
時々無言返しされますので、その場合は「...」に変換して返しています。
App.configの`<appSettings>`に、`A3RT_APIKEY`でAPIキーを指定してください。

```csharp:A3RTService.cs
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Net.Http;
using System.Threading.Tasks;

namespace ReplAndA3RT
{
    /// <summary>
    /// A3RTのAPIを呼び出すクラス
    /// </summary>
    class A3RTService
    {
        private string APIKEY;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        internal A3RTService()
        {
            // App.configから設定を取得
            APIKEY = ConfigurationManager.AppSettings["A3RT_APIKEY"];

            // 必要な情報が取得できない場合はエラー
            if (string.IsNullOrEmpty(APIKEY))
            {
                throw new ArgumentException("A3RT_APIKEY is empty");
            }
        }

        /// <summary>
        /// 対話APIを実行し、応答文を返す
        /// </summary>
        /// <param name="message">発言</param>
        /// <returns>応答</returns>
        internal async Task<string> MakeRequestAsync(string message)
        {
            HttpClient client = new HttpClient();

            // リクエストURL
            string uri = $"https://api.a3rt.recruit-tech.co.jp/talk/v1/smalltalk";

            // リクエスト本文
            var content = new FormUrlEncodedContent(
                new Dictionary<string, string>
                {
                    { "apikey", APIKEY },
                    { "query", message }
                });

            // APIを実行し、レスポンス本文を取得
            HttpResponseMessage response = await client.PostAsync(uri, content);
            JObject rss = await response.Content.ReadAsAsync<JObject>();

            // HTTPステータスが異常であればエラー
            if (!response.IsSuccessStatusCode)
            {
                throw new ApplicationException(
                    $"API failed. code={response.StatusCode}");
            }

            // 何も返さなかった場合は、何か埋めて返しておく
            if ((int)rss["status"] == 2000)
            {
                return "...";
            }
            // APIがエラーを出していればエラー
            else if ((int)rss["status"] != 0)
            {
                Console.WriteLine(rss);
                throw new ApplicationException(
                    $"API failed. ");
            }

            // レスポンス本文から応答文のみを抽出して返す
            return (string)rss["results"][0]["reply"];
        }
    }
}
```

### [Repl-AI](https://repl-ai.jp)を呼び出すプログラム

対話前に「[ユーザーID取得](https://repl-ai.jp/docs/references/registration)」を呼び、実際の会話では「[対話](https://repl-ai.jp/docs/references/dialogue)」を呼びます。
App.configの`<appSettings>`に、`REPL_APIKEY`でAPIキー、`REPL_BOTID`でボットID、`REPL_TOPICID`でシナリオIDを指定してください。

```csharp:ReplService.cs
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Configuration;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

namespace ReplAndA3RT
{
    /// <summary>
    /// Repl-AIのAPIを呼び出すクラス
    /// </summary>
    class ReplService
    {
        private string APIKEY;
        private string BOTID;
        private string TOPICID;

        // registration APIで取得したユーザーID
        private string id;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        internal ReplService()
        {
            // App.configから設定を取得
            APIKEY = ConfigurationManager.AppSettings["REPL_APIKEY"];
            BOTID = ConfigurationManager.AppSettings["REPL_BOTID"];
            TOPICID = ConfigurationManager.AppSettings["REPL_TOPICID"];

            // 必要な情報が取得できない場合はエラー
            if (string.IsNullOrEmpty(APIKEY) || string.IsNullOrEmpty(BOTID) || string.IsNullOrEmpty(TOPICID))
            {
                throw new ArgumentException("REPL_APIKEY/REPL_BOTID/REPL_TOPICID is empty");
            }

        }

        /// <summary>
        /// 「ユーザーID取得」APIを呼び出し、ユーザーIDをインスタンス内に設定する
        /// </summary>
        /// <returns></returns>
        internal async Task RegisterAsync()
        {
            HttpClient client = new HttpClient();

            // リクエストURL
            string uri = $"https://api.repl-ai.jp/v1/registration";

            // リクエストヘッダ
            client.DefaultRequestHeaders.Add("x-api-key", APIKEY);
            
            // リクエスト本文
            var json = JsonConvert.SerializeObject(
                new { botId = BOTID });
            
            // APIを実行し、レスポンス本文を取得
            HttpResponseMessage response = await client.PostAsync(
                uri, new StringContent(json, Encoding.UTF8, "application/json"));
            JObject rss = await response.Content.ReadAsAsync<JObject>();

            // HTTPステータスが異常であればエラー
            if (!response.IsSuccessStatusCode)
            {
                throw new ApplicationException(
                    $"API failed. code={response.StatusCode}");
            }

            // レスポンス本文からID部分を抽出し、インスタンス変数に設定
            id =  (string)rss["appUserId"];

            // APIキーが間違っている場合はidは空。ここでエラーを投げておく
            if (string.IsNullOrEmpty(id))
            {
                throw new ApplicationException(
                    $"registration API didn't return ID.");
            }
        }

        /// <summary>
        /// 「対話」を呼び出し、応答文を返す
        /// </summary>
        /// <param name="message">発言</param>
        /// <returns>応答</returns>
        internal async Task<string> MakeRequestAsync(string message)
        {
            HttpClient client = new HttpClient();

            // リクエストURL
            string uri = $"https://api.repl-ai.jp/v1/dialogue";

            // リクエストヘッダ
            client.DefaultRequestHeaders.Add("x-api-key", APIKEY);

            // リクエスト本文
            var json = JsonConvert.SerializeObject(
                new {
                    appUserId = id,
                    botId = BOTID,
                    voiceText = message,
                    initTalkingFlag = false,
                    initTopicId = TOPICID
                });

            // APIを実行し、レスポンス本文を取得
            HttpResponseMessage response = await client.PostAsync(
                uri, new StringContent(json, Encoding.UTF8, "application/json"));
            JObject rss = await response.Content.ReadAsAsync<JObject>();

            // HTTPステータスが異常であればエラー
            if (!response.IsSuccessStatusCode)
            {
                throw new ApplicationException(
                    $"API failed. code={response.StatusCode}");
            }

            // レスポンス本文から応答文のみを抽出して返す
            return (string)rss["systemText"]["expression"];
        }
    }
}
```

「[BOTで使える会話API・ライブラリ・サービスまとめ](https://qiita.com/kenzo0202/items/582e3a5e06b64ab24964)」で紹介されている他のボットとも会話させてみると、いいかもしれませんね。