この記事の要旨
この記事では、短時間で簡単にレコメンドを行うチャットボットを作る方法を紹介します。言語やフレームワークを問わず、Web APIを構築することさえできれば、誰でも簡単にレコメンド用のチャットボットを構築することが可能です。この記事では、質問に答えるとグルメの情報をレコメンドするチャットボットを例に作成方法を紹介します。もちろん、それ以外の用途にも活用できるので、興味をお持ちいただけたらぜひチャットボットを作ってみてください! (レコメンド以外でも、アンケートやインタビューを行うための利用も可能です👌 )
開発に要する所要時間は約30分程度です。
※この記事は個人の活動について紹介をしたものであり、所属する組織の活動とは一切関係はありません。
DEMO用のチャットボット
作成したチャットボットはブラウザで動作するチャットページやLINE、Slack上で動かすことができます。
利用するサービス・ツール
- mebo(ミーボ) 会話AI構築サービス
- Google Apps Script (GAS)
- ホットペッパー Webサービス (グルメサーチAPI)
mebo(ミーボ)
会話AIやチャットボットを簡単に構築して公開ができるWebサービスです。
公式サイト
このサービスは私が個人開発しているサービスで、2021年5月にリリースしました。会話AIを手軽に様々なプロダクトで利用ができることを目指して、日々改良を続けています。詳細な内容や使い方については下記の本にまとめました。
mebo(ミーボではじめる会話AI構築)
Google Apps Script
Google Apps Script(GAS)
本記事の内容を実現するために、簡易的なAPIをGASを使って用意しています。必ずしもGASである必要はありませんが、手軽に試せるので本記事では例として採用しています。必要に応じて他のプラットフォームや言語をご利用ください。
ホットペッパー Webサービス
グルメ情報をレコメンドするにあたり、「ホットペッパー Webサービス」のAPIを例として利用させていただいています。本記事では主にこのAPIとチャットボットを連携させておすすめのレストランをレコメンドする方法を紹介していきます。
※APIを使わないレコメンドチャットボットの実現方法についても触れます。
開発手順
GOAL
サンプルとして下記の要件を満たすレコメンドチャットボットを作成します。
- Webブラウザで会話ができる (LINEやSlackも可能)
- 「何か食べたい」とユーザが発話するとレコメンド用の会話が開始される
- ユーザの希望条件ごとにおすすめのレストランを表示できる
ユーザの希望条件
- 食べたいものを自由に指定できる
- Wifiの利用有無を選択できる
- 個室があるかどうかを選択できる
- クレジットカード利用有無を選択できる
サンプルということで、希望条件は適当に決めています。適宜ご変更ください。
手順1: meboでエージェントを作成する (3分)
下記のサイトからmeboにサインアップし、エージェントを作成します。(エージェントはチャットボット1体の単位です。)
mebo公式サイト
サインアップをしたら、画面左上の「新規作成して開始する」のボタンをクリックします。
エージェント作成に必要な項目が表示されるので入力し、最後に「登録して開始する」をクリックしましょう。
ダッシュボードに作成したエージェントが表示されればエージェントの作成は完了です。この段階で会話をプレビューするをクリックするとチャット画面で会話ができる状態になっています。
基本的に各画面の内容にしたがって操作をすれば問題ないですが、この手順について詳しく知りたい方はこちらの資料をご参照ください。
(参考) エージェントを新規作成して会話する
手順2: シナリオを作成する (10分~)
meboでは、エージェントが会話するコンテンツをさまざまな方法で登録できます。
ex.
- 一問一答の質問・応答ペアを登録する
- シナリオ対話を作成する
- エージェントになりきりながらインタビューに答えて発話を登録する
- AIに自動応答させる
今回は、おすすめのレストランを取得するための情報をユーザから聞き出す必要があるため、「シナリオ対話」を利用します。
(参考) meboに登場する用語について
(参考) シナリオで会話のフローをつくる
meboの「会話のトレーニング」の画面から「シナリオ一覧」を開きます。ページ左上にある「新しいシナリオの作成」をクリックしましょう。
シナリオエディタが起動するので、まずはシナリオの設定をします。「シナリオの設定を表示」を押して、設定画面を表示します。
上記の例では、シナリオの名前とトリガーを下記のように設定しました。
シナリオ名: グルメレコメンド
トリガー: 「特定の発話があった場合」
発話: 「何食べようか, 何か食べたい, おすすめのレストランを教えて」
これで「何か食べたい」や「おすすめのレストランを教えて」と話しかけると、このシナリオが起動するようになります。
次にノードを追加していきます。ノードはシナリオにおける会話の発話を行う1単位です。シナリオエディタにおける1つのアイテム(四角形)が1つのノードになります。
ノードには「質問」や「発話」「フリートーク」が設定できます。ノードはエッジを設定して他のノードと接続します。
エッジはノード間を遷移するための条件を設定します。
このノードとエッジを組み合わせて会話のシナリオを構築します。
簡易的なレコメンドチャットボットであればノードを分岐させてシナリオだけで作成することも可能ですが、今回は外部APIと組み合わせるため、分岐はさせずにノードを直列に接続していきます。
追加するノード
今回例として追加するノードを列挙します。
1. 説明
ノード種別: 発話
タイトル: 説明
発話: 「これからグルメのレコメンドをします。レコメンドに必要な内容についてご回答ください。」
ステート名: intro
クイックリプライ: 「わかりました」
※以後はイメージ添付は省略します。
2. 食べたいもの
ノード種別: 質問
タイトル: 食べたいもの
発話: 「今、何が食べたい気分ですか?」
ステート名: 「keyword」
クイックリプライ: 「そば」「うどん」「カレー」
3. 予算
ノード種別: 質問
タイトル: 予算
発話: 「予算はどれくらいですか?」
ステート名: 「budget」
クイックリプライ: 「~500円」 「501~1000円」 「1001~1500円」 「1501~2000円」 「2001~3000円」 「3001~4000円」 「4001~5000円」 「5001~7000円」 「7001~10000円」 「10001~15000円」 「15001~20000円」 「20001~30000円」 「30001円~」
「選択肢からのみ回答」にチェック✅
※「選択肢からのみ回答」にチェックが入っている場合、クイックリプライにある項目のみユーザから入力を受け付けるようになります。
4. wifi
ノード種別: 質問
タイトル: wifi
発話: 「wifiは利用しますか?」
ステート名: 「wifi」
クイックリプライ: 「利用する」「利用しない」
「選択肢からのみ回答」にチェック✅
5. 個室
ノード種別: 質問
タイトル: 個室
発話: 「個室を希望しますか?」
ステート名: 「privateRoom」
クイックリプライ: 「希望する」「希望しない」
「選択肢からのみ回答」にチェック✅
5. クレジットカード
ノード種別: 質問
タイトル: クレジットカード
発話: 「クレジットカードは利用する予定ですか?」
ステート名: 「creditCard」
クイックリプライ: 「利用する」「利用しない」
「選択肢からのみ回答」にチェック✅
6. レコメンド
ノード種別: 発話
タイトル: レコメンド
発話: 「おすすめは、${gourmet_api}です。」
クイックリプライ: 「ありがとう」
※${gourmet_api}
には、後ほど設定するレコメンド結果が格納されます。
以上のノードが設定できたら、ノード同士をエッジで接続していきましょう。
ノードの右端の点から接続先のノードの左端の点へドラックして線を引くことができます。
特に条件に分岐はないので、一律で「何らかの文字列が入力されている」という条件を設定しましょう。
すべてのノードが繋ぎ終わったら、右下の「変更を保存」をクリックして、シナリオを保存します。
ここまでを終えると、シナリオを実際の会話で試すことができます。会話を試す場合は、左上にある「プレビュー」ボタンをクリックしましょう。
トリガーに設定した発話を入力すると、シナリオが開始することが確認できれば成功です。
先ほど設定した${gourmet_api}
に格納する情報は設定されていないので、レコメンドはまだ行われません。
手順3: グルメレコメンドAPIを作る (10分)
meboで作成したエージェントから呼び出すレコメンドAPIをGASを利用して構築します。
API利用のイメージ
APIを構築する前に、meboとGASで作ったAPIの関係性についてイメージを深めましょう。
説明の便宜上今からGASで作成するAPIを「レコメンドAPI」と呼ぶこととします。
meboエージェントからAPIが呼ばれる際、レコメンドAPIにはエージェントが会話の中で取得したユーザのステート情報が送られてきます。
ステートは、シナリオの各ノードで設定したステート名にユーザの回答が紐づく形で保存されていきます。
今回設定したシナリオをユーザが回答すると、下記のようなステート情報がAPIに送られてきます。
{
"keyword": "ラーメン",
"budget": "501~1000円",
"wifi": "利用しない",
"privateRoom": "希望しない",
"creditCard": "利用する",
}
このJSONの情報をもとにホットペッパーWebサービスのAPIを利用しておすすめのグルメ情報を取得し、エージェントにその結果を返します。
レコメンドAPIのmeboエージェントへのレスポンスでは、エージェントが行う発話や表示する画像、リンクのURL等を指定できます。
GASでAPIを構築する
それではGASでAPIを作成していきます。
1. スプレッドシートを用意
新規のGoogleスプレッドシートを用意しましょう。
https://www.google.com/intl/ja_jp/sheets/about/
スプレッドシートファイルに任意の名前をつけ、「ログ」というシートを用意しましょう。
このシートには、後ほどAPIへのリクエストのログを記録していきます。
Apps Scriptが起動したら任意の名前をつけましょう。
2. ホットペッパーWebサービスの登録
ホットペッパーWebサービスのページにアクセスし、「新規登録」を行いましょう。
https://webservice.recruit.co.jp/doc/hotpepper/reference.html
新規登録が完了すると、メールでAPIキーが送信されます。そのAPIキーを後ほど実装で利用します。
3. 実装
下記が使用するコード全文です。APIキーとスプレッドシートのIDは適宜置き換えてください。
スプレッドシートIDはスプレッドシートのURLに含まれるIDです。
const apiKey = <ホットペッパーWebサービスのAPIキー>
const apiResponseFormat = "json"
function doPost(e) {
const params = JSON.parse(e.postData.getDataAsString());
const userState = params.userState;
const requireWifi = userState.wifi == "利用する";
const requirerequirePrivateRoom = userState.wifi == "希望する";
const requireCreditCard = userState.creditCard == "利用する";
const gourmet = getGourmet(userState.keyword, convertBudgetNameToCode(userState.budget), requireWifi, requirerequirePrivateRoom, requireCreditCard);
var res = {};
if (gourmet != null) {
res = {
utterance: gourmet.genre.catch + "「" + gourmet.name + "」はいかがでしょうか?",
options: [],
imageUrl : gourmet.photo.mobile.l,
url: gourmet.urls.pc,
extensions: null
};
} else {
res = {
utterance: "申し訳ございません、レコメンドできる情報が見つかりませんでした。",
options: [],
imageUrl : "",
url: "",
extensions: null
};
}
payload = JSON.stringify(res);
ContentService.createTextOutput();
var output = ContentService.createTextOutput();
output.setMimeType(ContentService.MimeType.JSON);
output.setContent(payload);
outputLog(payload);
return output;
}
function getGourmet(keyword, budgetCode, requireWifi,requirePrivateRoom, requireCreditCard) {
const wifi = requireWifi == true ? 1 : 0
const privateRoom = requirePrivateRoom == true ? 1: 0
const creditCard = requireCreditCard == true ? 1 : 0
var gourmetApiUrl = "http://webservice.recruit.co.jp/hotpepper/gourmet/v1/?key="
+ apiKey
+ "&keyword=" + keyword
+ "&wifi=" + wifi
+ "&budget=" + budgetCode
+ "&private_room=" + privateRoom
+ "&credit_card=" + creditCard
+ "&service_area=SA11&format=" + apiResponseFormat;
var response = UrlFetchApp.fetch(gourmetApiUrl);
var json = JSON.parse(response.getContentText());
if (json.results.shop.length > 0) {
console.log(json.results.shop[0]);
return json.results.shop[0];
}
return null;
}
function convertBudgetNameToCode(budgetName) {
var budgetApiUrl = "http://webservice.recruit.co.jp/hotpepper/budget/v1/?key=" + apiKey + "&format=" + apiResponseFormat;
var response = UrlFetchApp.fetch(budgetApiUrl);
var json = JSON.parse(response.getContentText());
var budgetCode = "B001"
json.results.budget.forEach(function(budget) {
if (budget.name == budgetName) {
budgetCode = budget.code;
}
})
return budgetCode;
}
function outputLog(txt) {
var id = <スプレッドシートのID>;
var spreadSheet = SpreadsheetApp.openById(id);
var sheetName = "ログ";
spreadSheet.getSheetByName(sheetName).appendRow(
[new Date(), txt]
);
}
各メソッドの内容を簡単に説明していきます。
doPost
function doPost(e) {
const params = JSON.parse(e.postData.getDataAsString());
const userState = params.userState;
const requireWifi = userState.wifi == "利用する";
const requirerequirePrivateRoom = userState.wifi == "希望する";
const requireCreditCard = userState.creditCard == "利用する";
const gourmet = getGourmet(userState.keyword, convertBudgetNameToCode(userState.budget), requireWifi, requirerequirePrivateRoom, requireCreditCard);
var res = {};
if (gourmet != null) {
res = {
utterance: gourmet.genre.catch + "「" + gourmet.name + "」はいかがでしょうか?",
options: [],
imageUrl : gourmet.photo.mobile.l,
url: gourmet.urls.pc,
extensions: null
};
} else {
res = {
utterance: "申し訳ございません、レコメンドできる情報が見つかりませんでした。",
options: [],
imageUrl : "",
url: "",
extensions: null
};
}
payload = JSON.stringify(res);
ContentService.createTextOutput();
var output = ContentService.createTextOutput();
output.setMimeType(ContentService.MimeType.JSON);
output.setContent(payload);
outputLog(payload);
return output;
}
POSTリクエストを受け取ったタイミングで呼ばれるメソッドです。引数eには、エージェントが会話で取得したユーザのステート情報が格納されています。格納されているその情報を取得し、ホットペッパーのAPIを呼ぶgetGourmetメソッドに渡す形に変換しています。そして、ホットペッパーのAPIから取得できた情報からエージェントに返すレスポンスを作成しています。
エージェントから送信されるリクエストや返すべきレスポンスの仕様の詳細は下記の資料をご参照ください。
(参考) 外部のAPIと連携する
getGourmet
function getGourmet(keyword, budgetCode, requireWifi,requirePrivateRoom, requireCreditCard) {
const wifi = requireWifi == true ? 1 : 0
const privateRoom = requirePrivateRoom == true ? 1: 0
const creditCard = requireCreditCard == true ? 1 : 0
var gourmetApiUrl = "http://webservice.recruit.co.jp/hotpepper/gourmet/v1/?key="
+ apiKey
+ "&keyword=" + keyword
+ "&wifi=" + wifi
+ "&budget=" + budgetCode
+ "&private_room=" + privateRoom
+ "&credit_card=" + creditCard
+ "&service_area=SA11&format=" + apiResponseFormat;
var response = UrlFetchApp.fetch(gourmetApiUrl);
var json = JSON.parse(response.getContentText());
if (json.results.shop.length > 0) {
console.log(json.results.shop[0]);
return json.results.shop[0];
}
return null;
}
ホットペッパーWebサービスの店名サーチAPIを利用して、レストランの情報を取得しています。エージェントから送られてきた情報をもとにリクエストパラメータを作成しています。
※あくまでサンプルなのでサービスのエリアはSA11
(東京)に決め打ちしています。適宜パラメータは変更してください。
convertBudgetNameToCode
function convertBudgetNameToCode(budgetName) {
var budgetApiUrl = "http://webservice.recruit.co.jp/hotpepper/budget/v1/?key=" + apiKey + "&format=" + apiResponseFormat;
var response = UrlFetchApp.fetch(budgetApiUrl);
var json = JSON.parse(response.getContentText());
var budgetCode = "B001"
json.results.budget.forEach(function(budget) {
if (budget.name == budgetName) {
budgetCode = budget.code;
}
})
return budgetCode;
}
ホットペッパーWebサービスの店名サーチAPIのリクエストで利用する予算IDを取得するためのメソッドです。エージェントから送られてくる予算の文字列を予算IDに変換します。
outputLog
function outputLog(txt) {
var id = <スプレッドシートのID>;
var spreadSheet = SpreadsheetApp.openById(id);
var sheetName = "ログ";
spreadSheet.getSheetByName(sheetName).appendRow(
[new Date(), txt]
);
}
GASに送られてきたリクエストを確認するために、スプレッドシートにログを出力します。
4. デプロイ
ソースコードが入力できたら右上の「デプロイ」を押して、デプロイの作業を始めましょう。
「新しいデプロイ」を選択します。
種類の選択から「ウェブアプリ」を選択します。
設定の各項目に下記を入力します。
説明文: 任意
次のユーザとして実行: 自分
アクセスできるユーザー: 全員
初回デプロイはアクセスの承認を要求されるので、「アクセスを承認」をクリックします。
下記の画面が表示された場合は、左下の詳細から「xxx(安全でないページ)に移動」をクリックします。
この画面が表示されればデプロイは完了です。ウェブアプリのURLをコピーしましょう。
手順4: meboにAPIを登録する (2分)
作成したレコメンドAPIのURLがコピーできたらmeboの管理画面に戻り、「API一覧」画面を開きましょう。
API一覧画面を開いたら、左上の「APIを追加する」をクリックします。
任意のAPI名とAPIのラベルgourmet_api
を指定し、先ほどコピーしたURLを貼り付けましょう。
入力が完了したら。「登録する」をクリックします。これでAPIとmeboで作成したエージェントが疎通できるようになりました。
公開設定画面を開き、「会話をテストする」をクリックして、動作を確認してみましょう。
「何か食べたい」と話しかけ、シナリオが開始したら最後まで会話をしてみましょう。APIが返したグルメ情報がチャット画面上で表示されれば成功です。
先ほど用意したスプレッドシートにもログが出力されているはずです。
補足事項
エージェントの公開
作成したエージェントは公開してURLを配布できます。また、公開したエージェントはLINEやSlackから利用ができる他、API経由で利用することもできます。本記事のエージェントはサンプルなので使い道はあまりないと思いますが、気になる方は下記をご参照ください。
(参考) エージェントを公開する
(参考) 作成したエージェントとLINEで会話する
(参考) 作成したエージェントとSlackで会話する
(参考) 作成したエージェントとAPI経由で会話する
GASで作成したAPIについて
今回はGoogleスプレッドシート + Apps ScriptでAPIを作成しましたが、こちらは本運用向きの構成ではありません。あくまで動作を試すためにご活用いただき、本運用する場合は、APIサーバを立てるのに最適なプラットフォームをご利用ください。
レコメンドの拡張について
本記事で紹介した内容は、「レコメンド」といいつつ、外部のAPIからグルメ情報を取得して表示をしているだけです。APIを叩くだけではなく、もっとレコメンドチックなことをやりたい場合は、前述した「ステート」を上手く利用してレコメンド機能を実装することも可能です。ステートには、ユーザが回答したさまざまな情報がkey-value形式で格納されるので、例えば下記のような方法を使ったレコメンドが考えられます。
Elasticsearchの文書検索を利用したレコメンド
レコメンドするコンテンツのステートに関する情報をElasticsearchでdocにしておき、エージェントから送られてきたユーザのステートをQueryに含めることによって、類似度の高いコンテンツを検索します。
分散表現を用いた類似度計算によるレコメンド
Word2Vecのような分散表現を利用して、レコメンドをすることも可能です。レコメンドするコンテンツにあらかじめステートごとの情報を持たせておき、ユーザのステートの情報と類似度を計算します。そして、類似度の合計が最も高いコンテンツをレコメンドする等します。
BERTを用いた類似度検索によるレコメンド
「分散表現を用いた類似度計算によるレコメンド」と主旨は同じですが、高精度な類似度計算をさせたい場合はBERTの利用も選択肢の1つです。具体的な例としては、「Sentence-BERT」というBERTを改良(ファインチューニング)して高精度な文ベクトルを作ることに特化した手法を用いて類似度を計算します。(BERTをそのまま利用しても類似度計算はできますが、Sentence-BERTと比較して計算効率が桁違いに悪いです。)
まとめ
meboとGASを使って手軽にレコメンドチャットボットを作る方法についてご紹介させていただきました。今回は「レコメンド」という用途でしたが、他にも様々な用途に使えるチャットボットを簡単に構築することができます。ぜひこの記事を参考に、ご活用いただければ幸いです。
尚、meboはmakunugiの個人開発プロダクトです。日々改良を続けていますので、機能の不具合やご要望等ありましたらTwitterアカウントまでお寄せいただけますと、大変励みになります。
Twitterアカウント
最後までお読みいただきましてありがとうございました。