プログラマーのメモ書き

伊勢在住のプログラマーが気になることを気ままにメモったブログです

OSC名古屋 2019 に出展してきました

先日(2019年7月16日)、OSC名古屋が開催されました。

今年もここで、『三重勉強会・コミュニティ共同ブース』として出展してきました。

当日の様子

会場の雰囲気はこんな感じ。

f:id:junichim:20190716100530j:plain

ブースはこちら。

f:id:junichim:20190716100624j:plain

今年は三重で開催されている勉強会や各種コミュニティを紹介する一覧表を作って、それを置いておきました。 チラシを置いておくだけなので人手はいらないのですが、当日は私と Hackers:桑名 の方の二人で対応しました。

ちなみに当日配ったチラシはこんな感じです。

f:id:junichim:20190716100653j:plain

せっかくなので、リンクも貼っておきます。

ご興味を持たれた方、ぜひその団体さんをフォローして、イベント情報などをチェックしてみてください。

今年の印象

去年のOSC名古屋に出たときの記事で、なんでこんなことしているのかについて書いたので、それについては過去記事をご覧ください。

ざっくりした今年の印象だけ書いときます。
今年はなんとなく去年よりも、三重出身者の方で今は別のところに住んでいるという方が多くいらしてたように思えます。ぜひ、帰省のタイミングとかとイベント開催日が合ったときは気軽に顔を出していただければなと思います。

あと、新たにあったパターンとして、『あるイベント等を三重県で開催したいので、既存にどんなコミュニティがあるのか知りたい』というような主旨の方もいらっしゃいました。もともと三重って勉強会やコミュニティの活動が少ないので、なかなか情報が集められないというのがあるんでしょうね。ぜひ、三重でも新しい活動を始めていただけるとありがたいです。

少しでも、三重の勉強会・コミュニティさんの活動が活発になることを願います。

最後に

元々は名古屋の勉強会さんのほうで『名古屋勉強会・コミュニティ協同ブース』という形で出されたいたのをまねて、OSC名古屋2017 からこういった共同ブースでの出展をやってます。
地方だと自分たちの地元で OSC が開催されていなくても、近隣で OSC がある場合は、こういった形で自分たちの地域にもコミュニティがあるよ、とPRするのも一つの方法なんじゃないかと思います。

追記

大事なの書くの忘れてました。

OSC名古屋参加時のアンケートに答えたら、モバイルバッテリー当たりました。

f:id:junichim:20190716161740j:plain

ありがとうございます。

ということで、いいことあるかもしれないので、お近くの OSC に遊びに行きましょう!

Dialogflow のチュートリアルを試してみた : events を使って書き換え (2/2)

Dialogflow のチュートリアルを試した続きです。

blog.mori-soft.com

上の記事で Dialogflow のチュートリアルを試しまして。ここでは、 context と fulfillment を使って、会話の流れを制御しています。

個人的には、このスタイルだと、 context を操作しているのが、 Intent の output context の箇所と fulfillment のコードに分かれていて、ちょっと気持ち悪いなと感じています。

Dialogflow のドキュメントを読むと Events という機能があり、ユーザー定義の Custom Event も扱えるようです。

そこで、この Events 機能を使い、context を直接操作するのではなく、 Event により Intent を切り替えて、 Intent の output context でコンテキストを制御するようなスタイルにしてはどうかと考えました。そうすると、

  • contexts : Intent で制御
  • events : fulfillment で制御

と分けることができるので、すっきりするかな?という目論見です。 ということで、やったことをメモっときます。

Events 機能について

Events 機能そのものについては、公式ドキュメントなどを参照してください。

あと、下記の記事なども参考にさせていただきました。

Dialogflowのeventを使ってみる。 - Qiita

Events を使った場合の処理

元々のチュートリアルでは、下記のところで context を切り替えていました。

  • fulfillment の checkAppintment 関数:予約が埋まっていた場合に context を reservation-followup から reservation-suggestion に切り替え
  • fullfillment の suggestAppintment 関数:『提案』機能をユーザーが了解したのち、 context を reservation-suggestion から reservation-followup に切り替え(元の予約のコンテキストに戻す)

これを Events を使う形にしたいと思います。

checkAppointment 関数でのイベント

Webhook からイベントを投げるには、 followupEventInput オブジェクトをレスポンスに追加してあげればOKです。
ここでは、 イベント名 suggest をパラメータ付きで投げます。

  if (flg) {
    response.fulfillmentText = `了解しました。 ${date} 日の ${time} 時の予約でよろしいでしょうか?`;
  } else {
    // event を発行
    response.followupEventInput = {
      name: "suggest",
      languageCode: "ja-JP",
      parameters: {
          date: date,
          time: time,
          suggested_time: "2019-07-22T13:00:00+9:00"
      }
    }

suggest イベントを受け取る Intent の追加

投げられた suggest イベントを受け取る Intent を新規に追加します。この Intent の名前を suggestion とします。

f:id:junichim:20190627143659p:plain

Intent を作成したら、 Events を開いて、『suggest』と入力して、保存すればOKです。これで、 suggest イベントが投げられれば、この Intent が呼ばれます。
また、今回はユーザー入力ではなくイベントにより呼び出すため、 Training phrases は空欄のままとします。

次に、 suggest イベントと共に渡されるパラメータを受け取るために、下記のようにこの Intent に対してパラメータを設定します。

f:id:junichim:20190627114054p:plain

パラメータの設定において、 Value 列に対して #event_name.parmeter_name という形式を指定することで、イベントパラメータで指定されたパラメータを Intent のパラメータとして扱うことが可能になります。このようにすると、この Intent の followup intent を定義した際に、パラメータを参照することが可能になります。

なお、Parameter Value については公式ドキュメントのこちらの記事などに詳しく記載されています。

Response の欄には、 suggest イベント発行により、この Intent が呼ばれた際に表示したい Response テキストを設定しておきます。

f:id:junichim:20190627114137p:plain

『最初に指定した日時が埋まっていたので、別の日程を提案しますよ』、という旨の文章を表示するようにします。

なお、この Intent に対する fullfilment は不要なので、 fulfillment は無効のままとしておきます。

suggestion インテントの followup インテント

上記の suggestion インテントへの応答を受け取るために、yes および no の followup インテントを作成しておきます。

f:id:junichim:20190627114638p:plain

reservation の時と同じく yes の followup インテントについては fulfillment を有効にしておきます。

suggestAppointment 関数でのイベント

この関数は、 suggest 処理で別の予約日時を示して、ユーザーが提案を受け入れる、とした際に呼ばれる処理になります。 イベントを使った場合は、 suggest インテントに対して、ユーザーが肯定の応答をた歳の、 suggestion - yes の Intent に対して呼ばれることになります。

なので、最初に Intent に応じて振り分ける処理を修正します。

    // intent と対応するハンドラ関数のマップを生成
    let intentMap = new Map();
    intentMap.set('reservation', checkAppointment);
    intentMap.set('reservation - yes', makeAppointment);
    intentMap.set('suggestion - yes', suggestAppointment);

chckAppointment 関数と同様に、イベントを投げるには、 followupEventInput オブジェクトをレスポンスに追加します。
今回は、イベント名 suggest-agree をパラメータ付きで投げます。

  // event を発行
  response.followupEventInput = {
    name: "suggest-agree",
    languageCode: "ja-JP",
    parameters: {
        suggested_time: suggested_time
    }
  };

なお、この関数の先頭で context を取得する際は、 suggestion-followup になるので、そこも修正しておきます。

async function suggestAppointment() {

  console.log("suggestAppointment called");

  const context = getContext(response, "suggestion-followup");

suggest-agree イベントを受け取るインテント

最後に、 suggest-agree イベントを受け取るインテント suggestion-agree を定義します。

f:id:junichim:20190627143925p:plain

suggest インテントと同様に、パラメータを定義して、イベントで投げられたパラメータをこの Intent のパラメータに割り当てます。

f:id:junichim:20190627141513p:plain

なお、fulfillment で context を切り替える方式だと、ユーザーに返す応答文も fulfillmentText で一緒に定義していましたが、イベントの場合は呼び出された Intent 側で設定する必要があります。

また、この Intent の output context を reservation-followup とすることで、提案がない場合のコンテキストへ戻すことを行っています。

動作

全体の動きは、チュートリアルと同じなので割愛します。イベントを使った処理の部分だけ、見ておきます。

ユーザーが『予約をしたい』と入力して、日付と時間も指定後、fulfillment 処理の中で、checkAppointment 関数がよばれます。 checkAppointment 関数では、(本来なら予約の有無を確認しますが、ここでは乱数で決めている)予約が取れない場合に、suggest イベントを発行します。

f:id:junichim:20190627142518p:plain

すると、上記のように(suggest イベントが発行されることで) suggestion インテントが呼ばれます(画面からはイベントが発行されたことはわかりません)。画面上では、想定していた Intent および Parameters の欄が想定通りになっていることが確認できます。 また、 context は suggestion-followup になり、返答を受け取る準備ができていることがわかります。

ここで、提案を受け入れるため、『はい』などとユーザーが入力すると、

f:id:junichim:20190627145600p:plain

のように、応答メッセージが表示されます。また、通常の応答処理になるように context が reservation-followup に切り替わっていることがわかります。

これで、fulfillment 内の context 切り替えで行っていた処理を events を用いて代替することができました。

ソース等はリポジトリをご覧ください。

比較

さて、公式チュートリアルを、 context の切り替えと events を用いた方法の2つで同じ処理を実現してみました。

当初のイメージとしては、後者のほうが見通しが良くなるのではないか?というのがあったのですが、実際に両方試した印象としては、どっちもたいして変わらないかな、という感じです (全然参考になってなくて、すいません・・・)。

元々、context と event の用途が違うためかもしれません。

ドキュメント等を読むと、context は文脈を定義し、その文脈を利用することで同じユーザー入力を区別するために用いるとあります。 一方、 event はユーザー入力を介せずに、Intent を呼び出すためにあります。

となると、これは比較するために用いた例が、違いを際立たせないという意味で、あまり良くなかったのかもしれません。 難しいなー。

ちなみに、前者はチュートリアルのタイトルが、『 Build an agent from scratch using best practices 』となっているので、このような例の場合は context の切り替えがベストなんだろうか?という気もしてます。

正直、もうちょっといろいろと組まないと使い分けの勘所は見えなさそうです。

まとめ

まあ、そうはいっても、これで一通り Dialogflow を使ってみることができました。このチュートリアルでは Custom Entities などは試していませんが、そんなにややこしい印象はないので簡単に試せるかと思います。

さて、Dialoflow を使う準備ができてきたので、おいおい LINE WORKS でのボットの改良を進めようかと思います。

Dialogflow のチュートリアルを試してみた : context と fulfillment (1/2)

LINE WORKS で予約投稿ボットを作成する件ですが、ボットと対話して相手先や投稿時間を指定できるようにしたいと思います。 ですが、AWS が用意しているチャットボット(など)のサービス Amazon Lex がいまだ(2019/6/18現在)に日本語未対応なので、他のサービスを使おうと思います。

一番下の参考に挙げたように、調べてみるといろいろなサービスが世の中にはあるようです。正直、何を使って実現するか迷うところです。
いろいろと調べて Dialogflow と Watson の2択まで絞ったのち、 両方簡単に試してみて、Dialogflow のほうが自分には合ってるっぽい印象だったので、Dialogflow を使ってみることに決めました。
無料で簡単に試せるのってありがたいですね。 Google, IBM さんともに感謝です。

まずは、 LINE WORKS とつなげて使う前に、 Dialogflow そのものをいろいろ試したので、それについてメモっときます。

チュートリアルについて

いろいろと、 Dialogflow サンプルもありますが、ここでは Dialogflow 公式にあるチュートリアルを試してみます。

Build an agent from scratch using best practices  |  Dialogflow

Build an agent from scratch using best practices (Advanced concepts)  |  Dialogflow

このチュートリアルでは、

  • パラメータの問い合わせ(Slot filling)
  • パラメータが埋まった際の fulfillment の呼び出し
  • コンテキスト(context)の切り替え

などを扱っています。また、チュートリアルが実現している機能として、

  • 予約の確認
  • 予約が取れなかった際の提案

をコンテキストを使い実現している当たりが参考になるかと思い選びました。

Webhook と Inline Editor

Dialogflow では、パラメータがすべてそろったら、指定した URLを呼び出して、何かを実行させたい場合、 fulfillment という機能が使えます(詳しくは、公式ドキュメントをお読みください)。

チュートリアルでは fulfillment の部分( Create a webhook with the inline editor および Build an agent from scratch using best practices (Advanced concepts) に記載されている処理)については、 Inline Editor (+ Dialogflow fulfillment library) で実現していますが、これを Webhook と JSON で書き換えてみました。

Inline Editor も無料で使えるのになんでわざわざWebhook を使うの?という理由の一つに、 Dialogflow の inline editor は無料プランでは外部API(外部のURLへのアクセス)が使えない、というのがあります。

【Dialogflowメモ】 Dialogflowを用いた外部APIの利用は無料ではできない!? | ゆたかみわーく

最終的には、 fulfuillment が呼ばれたときに、外部APIを使って情報を取得し、それを返そうと思っているので、このような構成で試すことにしました。

Dialogflow の Agent の作成

まずは Dialoflow の Agent を作成しないと始まりません。

適当な Agent を作成します。 Agent 作成時に GCP のプロジェクトをどうするか?という選択肢があるので、

f:id:junichim:20190619100925p:plain

デフォルトの 『Create a new Google project』を選んでおきます。あと、 default language は日本語にしときます。この default language は変更できないようなのでご注意ください(対応言語を追加することはできます)。

Agent ができたら、チュートリアルを先頭から実施していきます。

とりあえず、 fulfillment の手前 Create an intent with parameters まで実施します。 ここまで設定して、コンソール上(Dialogflow のシミュレータ)で思うような動作になっていることを確認しておきます。

問題がなければ、いよいよ fulfillment を試していきます。

fulfillment を設定

fulfillment は intent 単位で有効・無効を設定します。なので、ある Intent に対してはパラメータがそろったら fulfillment を呼び出すが、別の Intent に対しては呼ばない、といった設定が可能です。

Intent の設定画面の下のほうに、Fulfillment というところがあるので、そこを開いて、『Enable webhook call for this intent』を有効にします。

f:id:junichim:20190625095703p:plain

設定が終わったら Intent を保存しておきます。

次に、左側のメニューより Fulfillment を選び、Webhook か Inline Editor のどちらか一方を有効にします(どちらか一方しか有効にできません)。

f:id:junichim:20190625095937p:plain

ちなみに、Webhook は指定した URL (https) を POST で呼び出します(なお、 webhook の呼び出しの詳細は公式ドキュメントのここに載ってます)。
Inline Editor で定義した処理は GCP の Cloud Function で実行されます。

参考にしたこのチュートリアルでは(だけに限らず他の多くのサンプルでも) Inline Editor を使っていることが多い印象です。今回はここを Webhook (AWS Lambda) にしてみます。
ということで、 Webhook を有効にしたら、Lambda (実際には API Gateway )のURLを入力しておきます。

あと、 fulfillment の注意としては、呼び出し先( webhook にしても、Inline Editor にしても)は、Agent に対して一つだけが設定可能です。 つまり、複数のIntent からの fulfillment 呼び出しを有効にした場合、呼び出された側で処理の振り分け(ルーティング?っぽい処理)をしてやる必要があります。

呼び出し元(Intent)を区別するために必要な Intent 名などの情報は パラメータとして受け渡されるのでそれを利用して処理を記述します。

webhook の実装

Webhook の実装の前に残念なお知らせがあります。
実は、 Fulfillment 処理を簡単に書くための Node.js 用のライブラリがあります。

github.com

Dialogflow が Integration で対応している8つのチャットプラットフォームと Dialogflow 上のシミュレータに対応しているそうです。

Lambda 上の Node.js でも、これを使えば簡単に実装できそうに思えたのですが、このライブラリのメインで使うオブジェクトである WebhookClient オブジェクトを生成するために必要な request / response オブジェクトが Express のhttpオブジェクトとなってました。

Lambda の場合、どうやってこれに対応すればよいのか改めて調べるのもちょっと面倒だなと思ってしまいました(aws-serverless-express とか使うのか?)。

なので、今回はこのライブラリを使うのをあきらめました。 で、直接レスポンスで返す JSON を編集する方法で対応しました。ま、試用ですしね、今回はこれでいいかな、と。

以下では、JSONで処理する際のポイントについて説明していきます。最終的なコードは GitHubのリポジトリ をご覧ください。
なお、Dialogflow の試用が目的なので、予約が空いているかの確認や実際の予約処理は Google Calendar API は使わずに、適当に乱数で成否を返すようにします。

リクエストとレスポンス

API Gateway 経由で Lambda が受け取るった POST リクエストは、 event.body にJSON 文字列として渡されます。なので、

    let body = JSON.parse(event.body);

として、JSONオブジェクトとして取得しておきます。

また、戻り値も JSON オブジェクトで返すので

// 戻り値
let response;

function createResponse() {
  let response = {
    fulfillmentText: "This is a text response",
    fulfillmentMessages: [],
    source: "example.com",
    payload: {},
    outputContexts: [
        {
            name: "projects/${PROJECT_ID}/agent/sessions/${SESSION_ID}/contexts/context name",
            lifespanCount: 5,
            parameters: {
                param: "param value"
            }
        }
    ],
    followupEventInput: {}
  };
  return response;
}

として、あらかじめレスポンスオブジェクトを定義したうえで、

    response = createResponse();
    requestBody = body;

    // レスポンスにコンテキストを(ディープ)コピー
    response.outputContexts = JSON.parse(JSON.stringify(body.queryResult.outputContexts));

のように、outputContexts をリクエスト body からコピー(ディープコピー)しておきます。

もし、 fulfillment 処理において outputContexts を変更する必要がなければ、リクエストと同じ値を戻し、 必要があれば、後続する処理で書き換えるようにするためです。

なお、これらのリクエスト/レスポンスの JSON の定義は、公式ドキュメントに記載されているので、そちらをご覧ください(上記のレスポンス JSON は、このページのレスポンスから fulfillmentMessages と payload を削除して作成しました)。

Lambda での実装時の注意

あと、 Lambda を使う上での注意として、Lambda は関数を実行する際のインスタンスを再利用します。

このため、下記にあるように、再利用時にモジュールレベルの変数が使いまわされる場合があります(初期化状態が維持されます)。

毎回、初期化されるのではないので、実装時は注意が必要ですね(今回、実装しててトラブるまで気づいてませんでした・・・)。

intent の振り分け

チュートリアルの Inline Editor の例の場合、

exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
  const agent = new WebhookClient({ request, response });

  function makeAppointment (agent) {
    ・・・(中略)・・・
    });
  }
  let intentMap = new Map();
  intentMap.set('Make Appointment', makeAppointment);  // It maps the intent 'Make Appointment' to the function 'makeAppointment()'
  agent.handleRequest(intentMap);
});

のように WebhookClient を作成して、 handleRequest を呼べば、 intentMap に従って、処理を振り分けてくれます。

JSON で処理する場合もやり方をまねします。 webhook 関数を呼び出した元の intent は リクエストオブジェクトの queryResult.intent.displayName に書かれています。

なので、上記と同様に、 Intent 名に応じて呼び出す関数を map オブジェクトに登録します。

    // intent と対応するハンドラ関数のマップを生成
    let intentMap = new Map();
    intentMap.set('reservation', checkAppointment);
    intentMap.set('reservation - yes', makeAppointment);
    intentMap.set('reservation - suggestion - yes', suggestAppointment);

※ Intent 名が チュートリアルは 『Make Appointment』となっているところを 『reservation』に変えていますが、特に理由があるわけではなく単なる気分です。

実際の振り分け処理用の関数はこんな感じになります。

async function handleRequest(intentMap, intent) {
  const intentName = intent.displayName;

  // 呼び出し
  if (intentMap.has(intentName)) {
    return intentMap.get(intent.displayName)();
  }

  console.log("no intent handler, intent: " + intentName);
  throw new Error("no intent hander");
}

この振り分け関数を下記のように呼び出せば、intentに応じた処理が呼び出せます。

    // intent に応じて処理
    let ret;
    try {
      ret = await handleRequest(intentMap, intent);
    } catch(e) {
      console.error("error occured: " + JSON.stringify(e));
      throw e;
    }

Dialogflow からの応答

チュートリアルの MakeAppointment 関数を見ると、

  function makeAppointment (agent) {
    ・・・(中略)・・・
    // Check the availability of the time slot and set up an appointment if the time slot is available on the calendar
    return createCalendarEvent(dateTimeStart, dateTimeEnd).then(() => {
      agent.add(`Got it. I have your appointment scheduled on ${appointmentDateString} at ${appointmentTimeString}. See you soon. Good-bye.`);
    }).catch(() => {
      agent.add(`Sorry, we're booked on ${appointmentDateString} at ${appointmentTimeString}. Is there anything else I can do for you?`);
    });
  }

のように add メソッドを呼ぶことで対応しています。

JSON であれば、

    response.fulfillmentText = `予約が完了しました。 ${date} 日の ${time} 時に予約をお取りしました。`;

のように、 fulfillmentText に応答分をセットすればOKです。

context の取り出し

次に context を区別するために、contextを取り出す方法です。

チュートリアルの Create a confirmation step を見ると、

  function makeAppointment (agent) {
    // Get the context 'MakeAppointment-followup'
    const context = agent.context.get('makeappointment-followup');

のように、context 名をパラメータにして get メソッドを呼ぶことで context を取得しています。

これを JSON の処理で行うために、 context が受け渡されているフォーマットを確認すると、下記のようにスラッシュ区切りの構造になっています。

    "outputContexts": [
        {
            "name": "projects/${PROJECT_ID}/agent/sessions/${SESSION_ID}/contexts/context name",

なので、 outputContexts の配列を調べ、nameプロパティの最後が求める context 名になっているかを見れば、 context を取得できそうです。

例えば、こんな風にしてみました。

function getContext(response, contextName) {
  for (let context of response.outputContexts) {
    const ary = context.name.split("/");
    if (ary[ary.length-1] === contextName) {
      return context;
    }
  }
  return;
}

呼び出す側は、

async function makeAppointment() {

  console.log("makeAppointment called");

  const context = getContext(response, "reservation-followup");
  if (!context) {
    console.warn("コンテキスト, " + "reservation-followup" + " が見つかりません。");
    throw new Error("no context");
  }

  const date = context.parameters.date;
  const time = context.parameters.time;

  // コンテキストをクリア
  context.lifespanCount = 0;

  // 予約 OK/NG を想定
  const flg = trueOrFalse(1);

  if (flg) {
    response.fulfillmentText = `予約が完了しました。 ${date} 日の ${time} 時に予約をお取りしました。`;
  } else {
    response.fulfillmentText = `すいません。なぜか予約に失敗しました。 ${date} 日の ${time} 時の予約を取り直してください。`;
  }
  return response;
}

のようにして、取得した context およびそれに紐づくパラメータを利用することができます。

なお、ここで一点注意が必要なのが、 Lambda 関数からのレスポンスの JSON に含まれている outputContexts が最終的に Dialogflow に渡され、それが対応する(fulfillmenを呼び出した) Intent の output context になります。

なので、contextを操作する際は、request オブジェクトではなく、 response オブジェクトに対して操作を行っています(最初に outputContexts をディープコピーでレスポンスオブジェクトにコピーしている理由です)。

context の削除

context を削除するには、 上記の例にもあるように、その context の lifespanCount を 0 にした JSON を戻してやればOKです。

context の切り替え

新しい context を追加するためには、新規の context オブジェクトを生成し、 outputContexts 配列に追加すればOKです。 そのうえで、既存の context を削除(lifespanCount を 0 に設定)すれば、切り替えができます。

今回だと、指定日時に予約があるかどうかをチェックする部分で、先に予約があった場合(試しなので乱数で決定してます)、コンテキストを reservation - suggestion に切り替えています。

// intent に応じた処理
async function checkAppointment() {

  console.log("checkAppointment called");

  const context = getContext(response, "reservation-followup");
  if (!context) {
    console.warn("コンテキスト, " + "reservation-followup" + " が見つかりません。");
    throw new Error("no context");
  }
  
  const date = requestBody.queryResult.parameters.date;
  const time = requestBody.queryResult.parameters.time;

  console.log("context before: " + JSON.stringify(response));

  // check OK/NG を想定
  const flg = trueOrFalse(1);

  if (flg) {
    response.fulfillmentText = `了解しました。 ${date} 日の ${time} 時の予約でよろしいでしょうか?`;
  } else {
    response.fulfillmentText = `ごめんなさい。 ${date} 日の ${time} 時は、既に予約が埋まってました。別の日に予約しますか?`;

    // コンテキストをクリア
    context.lifespanCount = 0;

    // suggestionコンテキストに切り替え
    let newContext = JSON.parse(JSON.stringify(context));
    newContext.name = setNewContext(newContext.name, "reservation-suggestion");
    newContext.lifespanCount = 3;
    newContext.parameters = {
      suggested_time: "2019-07-22T13:00:00+9:00"
    };
    console.log("new context: " + JSON.stringify(newContext));
    response.outputContexts.push(newContext);
  }
  
  console.log("context after: " + JSON.stringify(response));

  return response;
}

function setNewContext(oldContextName, newContextName) {
  let ary = oldContextName.split("/");
  ary[ary.length-1] = newContextName;
  return ary.join("/");
}

上記の処理では新しい context を生成する際、 context オブジェクトの name プロパティを正しい形式に合わせるのを簡単にするため、既存の context オブジェクトをコピーし、 context 名だけ変更するようにしています。

ちなみに、contextは複数持てますので、常に一つにする必要はありません(contextの管理は頑張ってやってください)。

なお、intent や context をどのように JSON で扱えばよいかは、

Contexts and fulfillment  |  Dialogflow

に記載されているので参考にしてください。

実装した結果

チュートリアルの内容を一通り実装したものは、 GitHub のリポジトリ をご覧ください。

Dialogflow の Agent の内容は基本的にチュートリアルと同じです(一部 Intent 名が異なっています)が、これもわかりやすいように、Export した zipファイルも置いておきます。

zip ファイルの使い方は、 公式ドキュメントを見てもらうほうが早いのですが、簡単に書いておくと、

  1. Dialogflow で新規に Agent を作成
  2. Agent の設定画面を開く
  3. zip ファイルを import する

と設定内容が再現できます。ただし、 fulfillment の webhook の URL はダミーになっているので、ご自分のサーバー等の URL を入力してください。

なお、Agent で zip ファイルの設定を import する場合も、 default language は変更されません(追加は可能)ので、ご注意ください。

まとめ

これで Dialogflow を一通り使ってみて、 fulfillment の呼び出し、 context の切り替えも試しました。

ただ、この状態だと

  • context の操作が Dialogflow 側の Intent の設定と fulfillment に分かれている

のが気になります。ちょっと大きな Agent を作ると context の流れが混乱して破綻しそうな雰囲気が漂ってます。

これへの対応方法として Event を使った方法が使えないか試してみようと思います。

参考

ボット開発用のサービス・ツールを紹介してくれているまとめ記事

最初の2つの記事は、いろんなサービス・ツールを4つに分類して紹介してくれてますが、正直、チャット提供企業のAPIとそれ以外の違いはわかるのですが、残り3つの中の中の分類の違いはよく分かんなかったです。いろいろと試すと見えてくるのかもしれませんね。

最後の記事は、サービス・ツールという観点ではなく、対話サービスそのものがどういうものなのか、という点について解説してます。対話サービスを実現するとはどういうことなのかちょっとわかった気がしました。

Dialogflow のサンプル

整理しきれないぐらい大量のサンプルやチュートリアルがありますが、私が参考にしたものを挙げておきます。

Watson のサンプル

3番目のWatson のチュートリアル試してて、挫折しました。もうちょっとやったらピンと来るのかな?