こんにちは、yutaです。

長く間が空いてしまいましたが、Dialogflowシリーズの第5回目をお届けします。

前回はFulfillmentの超基礎編として、固定された回答を返す処理を記述しました。

今回は応用編ということで、ついに動的に回答が変わるFulfillmentを書いていきたいと思います。

今回の応用編でも、超基礎編と同じく、天気についての質問に回答するやり取りを作りたいと思います。

ただし、今回は先にも書いた通り、その時の天気によって回答を変えるため、次のようなことをします。

お天気サイトのAPIを利用して天気情報を取得して返す。

つまり、会話は次のようになります。

人:今日の大阪の天気は?

bot:(今日の大阪の天気・・・)

bot:(お天気サイト君、大阪の天気を教えて。)

お天気サイト君:(今日の大阪は気温30℃、晴れやで。)

bot:今日の大阪は気温30℃、晴れです。

Dialogflowがお天気情報サイトに天気を確認して、返ってきた情報を回答するんですね。

では早速作っていきましょう。

いつものように、画像はクリックすると大きく表示することができます。見にくい場合はクリックしてみてください。

Fulfillmentを使って天気予報を返事をする

前提条件

1.APIのバージョンは第4回と同じく2.0になります。

2.今回天気予報を確認するAPIは「World Weather Online」のものを利用します。

この後にAPIを利用するキーの取得方法を記載しますので、あらかじめキーを取得してください。

APIは無償での利用には60日の期間が決まっており、期限が切れると利用できなくなります。

また、天気の情報は英語で返されます。

World Weather OnlineのAPIキーの取得方法と利用方法

World Weather Onlineにアクセスします。

https://www.worldweatheronline.com/lang/ja/

画面上部のメニューから「API」を選択してください。

次の画面に移動したら、画面中央より少し下の「TRY FREE FOR 60 DAYS」を選択します。

ログインフォームが出るので、どの手段でもよいので登録してログインしてください。

次の画面のリンク部分にキーの値が表示されているので、コピーして取得してください。

ちなみに、リンクの先は利用回数などが観られる分析ページです。

キーを含め、次のようにURLにパラメータを渡すことで天気情報を取得することができます。

https://api.worldweatheronline.com/premium/v1/weather.ashx?format=json&num_of_days=1&q=★都市名★&key=★キー値★

試しに★都市名★を「osaka」にして★キー値★を先ほどコピーしていただいた値に変更してURLにアクセスしてみてください。

ズラ~っとたくさんの情報が返ってきます。

上の例であげさせていただいたパラメータの変更や追加をしたい場合は、LOCAL WEATHERのドキュメントを参照してください。

3.この例ではInline Editorにスクリプトを記載してCloud Functions for Firebaseで実行します。

Cloud Functions for Firebaseで外部(Googleが提供する以外の)APIを実行する場合、Firebaseの有償プランに入る必要があります。

同じようにInline Editorを利用される場合は、FirebaseのBlazeプラン(従量課金プラン)に登録してから作業してください。

個人の練習程度であれば、Blazeプランでも料金は発生しないと思います。

Blazeプランへの加入については、詳しくは次の記事をご覧ください。

Entitiyの準備をする

今回は次のようなEntityを一つ用意してください。

この時重要なことは、reference valueをローマ字で入力することです。

こうすることで「大阪」という単語から「osaka」を参照できるようになります。

Entityを含め、要素の名称については基本的に画像で私が保存している通りの名称を付けたものとして進めさせていただきます。

intentの準備をする

まずは先ほどのEntity「weather_place」が存在するTraining PhraseとしないTraining Phraseを用意します。

Action and parametersのweather_placeのvalueは「$weather_place」にしてください。

「$weather_place」であれば、質問内でヒットした単語に用意されたEntityのreference valueが参照されます。

「$weather_place.original」にすると、質問で入力されたままの単語が参照されます。

今回は場所をローマ字で伝えなければならないので「$weather_place」にする必要があります。

weather_placeが存在しない質問も受け付けるため、promptも用意しておきます。

最後に、Fulfillmentの欄の「Enable webhook call for this intent」をONにして、webhook(Inline Editorの記述)を受け付けられるようにしましょう。

Fulfillmentの準備をする

まず最初に今回Inline Editorに記載する内容は次の通りです。

'use strict';

const https = require('https');
const http = require('http');
const functions = require('firebase-functions');

const host = 'api.worldweatheronline.com';
const wwoApiKey = '★キーの値★';
const { dialogflow } = require('actions-on-google');
const app = dialogflow();

app.intent('Hows_the_weather_of', (conv,{weather_place}) => {
    return callWeatherApi({weather_place})
        .then((output) => {
            conv.ask(output);
        }).catch(() => {
            conv.ask('ごめんなさい、わかりません。');
        });
});

function callWeatherApi (weather_place) {
  return new Promise((resolve, reject) => {
    let path = '/premium/v1/weather.ashx?format=json&num_of_days=1' +
    '&q=' + weather_place.weather_place + '&key=' + wwoApiKey;
    console.log('API Request: ' + host + path);

    http.get({host: host, path: path}, (res) => {
      let body = ''; // var to store the response chunks
      res.on('data', (d) => { body += d; }); // store each response chunk
      res.on('end', () => {
        let response = JSON.parse(body);
        let forecast = response['data']['weather'][0];
        let location = response['data']['request'][0];
        let conditions = response['data']['current_condition'][0];
        let currentConditions = conditions['weatherDesc'][0]['value'];

        let output = `Current conditions in the ${location['type']} 
        ${location['query']} are ${currentConditions} with a projected high of
        ${forecast['maxtempC']}°C or ${forecast['maxtempF']}°F and a low of 
        ${forecast['mintempC']}°C or ${forecast['mintempF']}°F on 
        ${forecast['date']}.`;

        resolve(output);
      });
    });
  });
}

exports.dialogflowFirebaseFulfillment = functions.https.onRequest(app);

*私も練習しながら作っております。確認しながらやっていますが、万が一コードがうまく動かない場合はご連絡ください。

今回のキモになる部分は、callWeatherApi の中ですぐにreturnしているPromiseです。

Promiseは中で実行される非同期通信(APIを参照して天気情報を取ってくる)の完了をきちんと待ってくれるオブジェクトです。

もし仮にAPI呼び出しで値を取ってきても、取り終わる前に次の処理が実行されてしまっては・・・困りますよね。

おそらくそれは空白を扱うか、エラーになります。

Promiseの中でAPIのパスを組み立てて、http.getでAPI呼び出しを実行します。

http.getでは、res.on('data',(d))...の行で、返ってきた情報を次々bodyに格納し、res.on('end',())...でレスポンス(情報が返ってくること)の終了後の処理を書きます。

res.on('end',())...では、取得した情報をJsonデータとしてパース(プログラムで扱えるように変換すること)し、ずらずらと並ぶ名前/値のペアから必要な個所を抜き出し、outputを組み立てて、resolveでPromiseの呼び出し元にoutputを返します。

resolveは、Promise内での非同期処理の成功時に呼ぶメソッドで、一方でエラーがあった場合はrejectを返します。

app.intentでは、callWeatherApiからoutputが返ってきたら、その内容を会話の答えとして回答します。

動作確認

さてではこの処理が正しく動くか動作確認をしてみましょう。

記載している返答方法では画面右側の「Try it now」で回答を得ることはできないので「Actions on Google」を開きましょう。

シミュレーターをテスト用アプリにつないで「大阪の天気は?」と確認してください。

正しく実装できていれば、画像の通り現在の大阪の天気が返ってきます。

Try it nowやWeb画面に返答を返したい場合の書き方は第4回の「Fulfillmentの結果をWeb Demoで確認したい」の項を参照してください。

以上になります。

最後まで読んでいただきありがとうございました。