kun432's blog

Alexaなどスマートスピーカーの話題中心に、Voiceflowの日本語情報を発信してます。たまにAWSやkubernetesなど。

〜スマートスピーカーやVoiceflowの記事は右メニューのカテゴリからどうぞ。〜

VoiceflowのCapture Stepがアップデートされました!

f:id:kun432:20210313005910p:plain

VoiceflowのCapture Stepが新しくなりましたのでご紹介します。

目次

これまでのCapture Step

Voiceflowには、ユーザからの発話を受け取って変数に入れるためのステップは主に以下の2つになります。

  • Capture Step
  • Choice Step

Choice Stepは、インテントごとに分岐させるためのステップです。そして、インテントごとにサンプル発話を用意しておいて、そこに含まれるスロットを変数として受け取ります。どのインテントにマッチしなかった場合の処理や、複数のスロットを受けるためのマルチターンの設定なんかもできて、とても柔軟です。ユーザからの発話の受け取とりはChoice Stepを使うのが基本になると思います。

f:id:kun432:20220115035923j:plain

f:id:kun432:20220115034249p:plain

これに対しCapture Stepは、ダイレクトにスロットだけを受け取りたい場合に使います。

f:id:kun432:20220115042619j:plain

f:id:kun432:20220115041309p:plain

Choice Stepに比べるととてもシンプルでかんたんそうに見えますよね。特にVoiceflowを触り始めたばかりだと、インテントやサンプル発話、スロットの概念もまだ難しいでしょうし、Choice Stepよりもこちらのほうがわかりやすく感じると思います。ただ、色々と問題がありました。

これまでのChoice Stepの問題点

これまでのChoice Stepが想定しているのは、以下のような会話フローになります。

ご注文は何にしますか?

コーヒー

コーヒーですね。ご注文ありがとうございました。

以下の通り問題なく動作します。

f:id:kun432:20220115043138p:plain

では以下のような発話の場合にどうなるか見てみましょう。

コーヒーをください

コーヒーにしようかな

f:id:kun432:20220115043439p:plain

f:id:kun432:20220115043430p:plain

コーヒーの前後も含めた形で取れてしまっていますね。Choice Stepではスロット単体の回答を期待しています。上記のような「人間らしい」発話からスロットだけを取り出すようなことができません。

ただ、上記の例ではカスタムタイプを使っていて、スロット値のサンプルは3つしかありません。これはちょっと少なすぎてうまく認識できなかったという可能性もあるかもしれませんね。ビルトインのスロットタイプであれば、認識精度は上がるはずです。少し変えてみましょう。

f:id:kun432:20220115044946p:plain

f:id:kun432:20220115044959p:plain

ビルトインスロットタイプに「飲み物」はありませんが、代わりに「食べ物」(AMAZON.Food)があります。これを使いましょう。レストランの注文という感じに変えてみました。さてどうなるでしょうか。

f:id:kun432:20220115045651p:plain

これはまあ当然として。

f:id:kun432:20220115045701p:plain

やっぱりダメですね・・・もっとひどいパターンだと・・・

f:id:kun432:20220115045711p:plain

こうなります。全然インテリジェントな音声アシスタントじゃないですよね。

今回パッと再現できなかったのですが、もっと酷いケースになるとそもそも何も値が取れない(0という値で渡ってくる)というケースもあります。発話がスロットとして認識されないようなケースですね。こういったことを回避するには、Captureで受け取った値が正しいか?をチェックするというようなエラー処理を追加する必要がでてきます。

これに対して、Choice Stepを使う場合だと、

  • サンプル発話とスロットをきちんと設定しておけば、スロット単体の値だけきちんと取れる
  • スロットが取れない≒インテントにマッチしない、ということでNo Matchを使ったエラー処理もできる(とはいえ、値チェックはやったほうが良いと思います)

ができます。

つまり、Capture Stepは一見シンプルでかんたんそうに見えますが、実際には、人間の会話のような柔軟性がなく、想定外の発話に対してとても弱かったんです。遠回りでもChoice Stepを使うほうが良いわけです。

新しくなったCapture Step

とはいえ、簡単にできるのであればやっぱりそっちを使いたいですよね。ということで、新しくなったChoice Step、どういったところが改善されているのかを見てみましょう。

新しいCapture Stepは、以前とちょっと場所が変わってUser Inputの下にあります。アイコンも変わっています。

f:id:kun432:20220116143629j:plain

これまでのChoice Blockと置き換えてみます。パッと見でわかるようにスロット値が正しく取れない(スロットとして認識されない)場合のNo Matchが追加されています。これは助かりますね。

f:id:kun432:20220116144430j:plain

ステップの設定を見てみましょう。先ほどお伝えしたとおりNo Matchのメニューが追加されていますね。ここはChoice Stepと同じなので割愛します。上のフォームのところにスロット名を入力すれば良さそうですね。

f:id:kun432:20220116152208p:plain

入力しようとすると「Entire user reply」というのが表示されますが、これは後ほど。普通にスロット名を入力します。

f:id:kun432:20220116153420p:plain

今回のサンプルでは"drink"としました。ENTERで確定させるか、"Create ..."をクリックします。

f:id:kun432:20220116153427p:plain

いつものスロットタイプの作成画面ですね。カスタムタイプでいくつかサンプル値をいれました。もちろんシノニムも使えます。

f:id:kun432:20220116153550p:plain

スロットタイプ作成後、ステップの設定画面に戻ってくると、新しい設定が増えていますね。

f:id:kun432:20220116154016p:plain

まずUtterancesですが、これがサンプル発話になります。上で書いたとおり、以前のChoice Stepではスロット単体の発話(「コーヒー」)しか想定できなかったのが、サンプル発話中のスロット(「コーヒーをください」)にも対応できるようになります。なお、スロット単体のサンプル発話は不要です(自動で追加されます。)

f:id:kun432:20220116155419j:plain

テストしてみるとちゃんと動いてるのがわかります。

f:id:kun432:20220116160740p:plain

f:id:kun432:20220116160747p:plain

スロット単体ももちろんこれまで通り動きます。シノニムも問題ないですね。

f:id:kun432:20220116160755p:plain

設定画面に戻ります。スロットタイプを作成したあとにもう一つ新しい項目「Entity Prompt」がありますね。

f:id:kun432:20220116161525j:plain

Capture Stepの設定でもう一つ追加されているEntity Promptは、スロットが取得できなかった場合にユーザに発話を促すための発話を設定します。No Matchとの違いがわかりにくいですね。わかりやすくするために別のサンプルを用意しました。

f:id:kun432:20220116170837p:plain

名前を聞いて挨拶をするというサンプルです。Capture Stepの設定では、スロット「name」とそれを含んだサンプル発話を設定しています。スロット「name」はAMAZON.FirstNameを使っています。

f:id:kun432:20220116171153p:plain

f:id:kun432:20220116171159p:plain

No Matchも設定してあります。

f:id:kun432:20220116173210p:plain

では、Entity Promptをこんな感じで設定しましょう。

f:id:kun432:20220116172645j:plain

f:id:kun432:20220116172659p:plain

試してみましょう。全然関係ないことを回答してみます。

f:id:kun432:20220116173007p:plain

サンプル発話にマッチしないということで、No Matchに設定した内容が発話されて、その後、Entity Promptの内容が発話されて、ユーザに発話のヒントを促しているわけですね。

ただ、これ別にNo Matchで設定しちゃえばよさそうですよね、わざわざ分ける必要はないようにも思えます。Entity Promptが生きるのは別のケースにあります。

以下をご覧ください。

f:id:kun432:20220116174052j:plain

そう、新しいCapture Stepでは、複数のスロットを取得することができるんですね。このケースでEntity Promptが役に立ちます。

では、名前に加えて、年齢を聞くようにしてみましょう。スロット名「age」を作りましょう。

f:id:kun432:20220116174542j:plain

スロットタイプはAMAZON.Numberですね。

f:id:kun432:20220116174622p:plain

サンプル発話もこんな感じで。

f:id:kun432:20220116174736j:plain

Entity Prromptで年齢を聞くように促します。

f:id:kun432:20220116175043p:plain

最後のSpeak Stepで年齢も答えるようにしておきます。

f:id:kun432:20220116175413p:plain

ではテストしてみましょう。

f:id:kun432:20220116175512p:plain

はい、それぞれのスロットが順番に聞かれるようになりましたね。

ただし、ここはいくつかおかしな動きもあります。全然関係ないことを答えるパターンも見てみましょう。

f:id:kun432:20220116182638p:plain

こんな感じで上手く言ってるように見えますね。もう一つ。

f:id:kun432:20220116183526p:plain

実はこれ最後の発話でスキルが終了してしまっています・・・

おそらくですが、Alexaの場合はダイアログモデルを使っていると思いますが、ダイアログモデルのスロット収集は2回失敗するとスキルが終了してしまうのでそのせいではないかと思います。

また、もう一つ。

f:id:kun432:20220116183828p:plain

2回目の年齢スロット収集で失敗した場合、再度名前の確認からに戻ってしまいますね。。。

一回の発話で複数スロットを設定する方法もあるみたいですが、ちょっとまだ挙動を抑えきれていません。複数のスロットを確実に受け取りたい場合は、現状はChoice Step を使うほうが良いかもしれません。

このあたりはもう少し確認が必要かなと思いますが、少なくともサンプル発話に対応できるのとNo Matchが使えるようになっただけでも、かなり便利になっていると思います!

「Entire user reply」とは?

スロット名をつける際に選択できる「Entire user reply」についても見てみましょう。これはスロットではなくマルッとユーザの発話を取得することができます。

少しサンプルを変えてみましょう。いわゆる「オウム返し」です。

f:id:kun432:20220116190113p:plain

Capture Slotで「Entire user reply」を設定して、スロット名を指定します。

f:id:kun432:20220116190202p:plain

試してみましょう。

f:id:kun432:20220116190501p:plain

ちゃんと動いていますね。Alexa開発者コンソールを見ると、SearchQueryで設定されているのがわかりますね。

f:id:kun432:20220116190808p:plain

ただし、Alexaの場合、自由な発話の受け取りを使ったスキルを公開する場合、審査でどう判断されるかが正直わかりません。過去にもいろんなやり方がありましたが、審査基準が変わって使えなくなったりしたこともあると思うので、こればかりは審査に出してみないとわかりませんね。自由な発話はユーザがどういう発話をするかが想定できないので、プライバシーの観点でも注意が必要かなと思います。

まとめ

以前のCapture Step、難しい事を考えなくても簡単に使えるということで、最初のうちは多用しがちなのですが、実際にAlexaで動かしてみるとうまく動かない、特に意図しない発話の場合に正しくスロット値が取れない、ということでフォーラムなど問い合わせが多かったです。個人的には、インテント・サンプル発話・スロットをきちんと理解した上でChoice Stepを使うというのがベストプラクティスだと思うのですが、どうしてもここの部分のハードルは高いのかもしれません。

今回のアップデートにより、以前に比べるとシンプルさは薄れた感はありますが、それでもChoiceに比べると覚えることは少なくて済みますし、サンプル発話やNo Matchに対応したことにより意図しない発話にも対応できます。自由な発話を受け取れるのも便利ですね。

使い分けとしてはこんな感じになるかなと思います。

  • Capture
    • 一つのコンテキストの中で一つのスロットを収集したい
    • 自由な発話を受け取りたい
  • Choice
    • 複数のコンテキストごとに、異なるスロットを収集して、分岐させたい
    • 一つのコンテキストで複数のスロットを最小限の手間で確実に収集したい

会話のコンテキストやユースケースにあわせて使い分けると良いと思います!

Voiceflow Dialog Management APIでSlackとつないでみた

f:id:kun432:20210313005910p:plain

VoiceflowのDialog Management APIをつかって、LINEにつづいて、Slackにつないでみました。

Dialog Management APIについてはこちら。

目次

構成

基本的な構成は前回と同じです。

f:id:kun432:20220107025735p:plain

コード

コードは以下で公開しています。

ではやってみましょう。

前提として、

  • Voiceflowのサンプルプロジェクトはすでにインポート済
  • 会話モデルのトレーニング済
  • APIキーやバージョンIDを取得済

とします。

サンプルプロジェクトはLINE連携と同じものを使用します。(右クリックでダウンロード)

https://raw.githubusercontent.com/kun432/voiceflow-samples/main/dialog-managment-api/dialog-management-api-sample-line.vf

プロジェクトのインポートとトレーニングのやり方

APIキーとバージョンIDの取得方法

Slackアプリの作成&設定その1

まず最初にSlack側でアプリを作成していきましょう。基本的に公式のドキュメントに従って準備していきますので、各項目の説明等はドキュメントを参照してください。

slack.dev

アプリの管理画面を開きます。

f:id:kun432:20220107030316j:plain

ブラウザが立ち上がります。右上の「ビルド」をクリック。

f:id:kun432:20220107031059j:plain

「Create an App」をクリック。

f:id:kun432:20220107031125j:plain

「From Scratch」をクリック。

f:id:kun432:20220107031206j:plain

アプリの名前と、アプリを使うワークスペースを選択して、「Create App」をクリック。

f:id:kun432:20220107031516j:plain

アプリが作成されました。

f:id:kun432:20220107032211p:plain

ではアプリの設定や連携に必要な情報を集めていきましょう。左のメニューにある「OAuth & Permissions」をクリックします。

f:id:kun432:20220107032341j:plain

OAuth & Permissionsページが開いたら、下の方にスクロールして、Bot Token Scopesのところにある「Add an OAuth Scope」をクリックします。

f:id:kun432:20220107032937j:plain

いくつか選択肢がありますが、アプリがボットとしてメッセージを投稿できる「chat.write」を選択します。

f:id:kun432:20220107033536j:plain

以下のようになっていればOKです。

f:id:kun432:20220107033608p:plain

上の方にスクロールして、左のメニューから「Install App」をクリックします。

f:id:kun432:20220107033803j:plain

「Install to Workspace」をクリックします。

f:id:kun432:20220107033931j:plain

権限の確認画面が表示されます。「許可する」をクリックします。

f:id:kun432:20220107034026j:plain

インストールが完了すると、「Bot User Auth Token」が発行されます。これが連携に必要な1つ目の情報になります。どこか安全な場所にペーストして保管しておきましょう。

f:id:kun432:20220107040600j:plain

次に左のメニューから「Basic Information」をクリックします。

f:id:kun432:20220107040935j:plain

下の方にスクロールして、「Signing Secret」のところで「Show」をクリックして、表示された文字列を安全な場所にペーストしておきます。これが連携に必要な2つ目の情報です。

f:id:kun432:20220107041418j:plain

これで連携に必要な情報は集まりました。もう少し設定は必要ですが、一旦、Node.jsアプリのほうに移りましょう。ブラウザは開いたままにしておいいてください(あとで戻ってきます)

Node.jsアプリ&ngrok起動

ではVoiceflowとSlackをつなぐためのNode.jsアプリを起動していきます。

レポジトリをクローンします。

$ git clone https://github.com/kun432/voiceflow-dialog-management-api-sample-ja
$ cd voiceflow-dialog-management-api-sample-ja

slack用のソースのディレクトリに移ります。

$ cd slack

パッケージをインストールします。

$ npm install

VoiceflowとSlackのAPIキーなどの環境変数を設定していきます。設定するのは以下。

  • SLACK_BOT_TOKEN
    • Slackアプリ作成時に取得した「Bot User Auth Token」を指定
  • SLACK_SIGNING_SECRET
    • Slackアプリ作成時に取得した「Signing Secret」を指定
  • VF_API_KEY
    • Voiceflowで作成したAPIキー
  • VF_VERSION_ID
    • VoiceflowのプロジェクトのバージョンID

"XXX...XXX"の部分をそれぞれ置き換えてコマンドを実行します、

$ export SLACK_BOT_TOKEN="XXX...XXX"
$ export SLACK_SIGNING_SECRET="XXX...XXX"
$ export VF_API_KEY="VF.XXX...XXX"
$ export VF_VERSION_ID="XXX...XXX"

Node.jsアプリを起動します。以下のように表示されればOKです。

$ npm start

> slack@1.0.0 start
> node index.js

⚡️ Bolt app is running on 3000!

ngrokを立ち上げて公開します。

$ ngrok http 3000
ngrok by @inconshreveable                                 (Ctrl+C to quit)

(...snip...)

Forwarding                    http://XXXX-XXX-XXX-XXX-XXX.ngrok.io -> http:
Forwarding                    https://XXXX-XXX-XXX-XXX-XXX.ngrok.io -> http

(...snip...)

上記で表示されるHTTPSのURLを記録しておきます。

ローカルの準備はこれで完了です。再度slack側の設定に戻ります。

Slackアプリの設定その2

ブラウザに戻ってSlackアプリの設定を続けます。

左のメニューから、「Event Subscriptions」をクリックします。

f:id:kun432:20220107144901j:plain

「Enable Events」のスイッチをONにします。

f:id:kun432:20220107145136j:plain

スイッチをONにするとURLを入力する欄が表示されるので、ngrokで表示されたURLを入力し、URLの末尾に/slack/eventsをつけます。

f:id:kun432:20220107145424j:plain

URLが正しければ上記のように「Verified」と表示されます。このとき、ngrokを立ち上げたコンソールでも以下のように表示されると思います。「200 OK」になっていれば問題ないです。

f:id:kun432:20220107145830j:plain

URLが間違っていたり、正しく動かない場合は以下のように表示されるので、ngrokが正しく立ち上がっているか、URLが正しいかを確認してください。

f:id:kun432:20220107150137j:plain

(上記の例は最後のsがたりない)

ただしくURLを入力できたら、下にスクロールして「Subscribe to Subscribe to bot events」をクリックします。

f:id:kun432:20220107150417j:plain

「Add Bot User Event」が表示されるのでこれをクリックします。

f:id:kun432:20220107150624j:plain

ボットがメッセージを受け取るイベントを選択します。とりあえず以下を選択します。

  • message.channels
    • ボットが追加されているパブリックチャンネルのメッセージを受け取ります
  • message.groups
    • ボットが追加されているプライベートチャンネルのメッセージを受け取ります
  • message.im
    • ユーザからボット宛(@でメンション)のメッセージを受け取ります
  • message.mpim
    • ボット宛のグループ DM を受け取ります

f:id:kun432:20220107151431p:plain

こんな感じに設定できればOKです。最後に一番下にある緑の「Save Changes」をクリックします。

f:id:kun432:20220107151633j:plain

SuccessとなればOKです。アプリを再インストールするように言われますので、「reinstall your app」をクリックします。

f:id:kun432:20220107151852j:plain

再度権限の確認画面が表示されますので「許可」をクリックします。

f:id:kun432:20220107152003j:plain

これですべての設定が完了しました。

動作確認

それではSlackアプリで動作確認してみましょう。適当なチャンネルの詳細画面を開いて、「インテグレーション」の「アプリの追加」をクリック。

f:id:kun432:20220107152806j:plain

作成したアプリ名が表示されますので「追加」をクリックします。

f:id:kun432:20220107152910j:plain

これでボットがチャネルに参加しますので、適当に話しかけてみてください。

f:id:kun432:20220107153004p:plain

ボットがメッセージを受け取るイベントの設定により、チャンネルのすべてのメッセージを受け取る or @メンションでのみ受け取る等の設定もできますので、ユースケースに合わせて選択すれば良いと思います。

また、Node.jsアプリを立ち上げたコンソールで、リクエスト・レスポンスなどをデバッグ表示していますので、実際にどういったデータがやりとりされているかを見ると良いでしょう。

f:id:kun432:20220107153612j:plain

なお、前回・前々回の記事でもご紹介していますが、VoiceflowのState APIを使うと、永続的な情報をVoiceflow側に持たせて、アプリからは一意のキーでアクセスするだけで、ユーザごとの永続的なアトリビュートの管理ができます。プロジェクト側で変数を保持するようにしておくと、以下のように、1回目のアクセスで聞かれた名前と年齢を2回目のアクセスでは聞かれない、ということができます。

f:id:kun432:20220107154156p:plain

SlackではユーザごとにID(message.user)が払い出されるようなので、これをキーにしています。テストなどでユーザ情報を削除したい場合は、Node.jsアプリのコンソールで表示されるSlackからのリクエストに含まれるユーザIDを取得して、

f:id:kun432:20220107160223j:plain

以下のような感じでDELETEリクエストを送れば削除されます。

$ USER_ID="XXXXXXXX"
$ curl -s -X DELETE "https://general-runtime.voiceflow.com/state/$VF_VERSION_ID/user/$USER_ID" -H "Authorization: $VF_API_KEY"

まとめ

LINEにつづいてSlackでも連携してみました。チャットアプリとVoiceflowをつなぐ中間アプリを用意するだけで、Voiceflowの会話ロジックは共通で、いろんなフロントエンドに対応できるのがDialog Management APIのメリットだと思います。

Voiceflow Dialog Management APIでLINEとつないでみた

f:id:kun432:20210313005910p:plain

前回ご紹介したVoiceflowのDialog Management APIをつかって、LINEとつないでみました。

目次

構成

Voiceflow SDKでLINEとつないだときと基本的に同じです。

f:id:kun432:20220104013817p:plain

ローカルに立てたNode.jsアプリにngrok経由でLINE Messaging APIかつなぎこみます。Herokuあたりを使えばインターネット経由で動かすこともできると思います。

コード

コードは以下で公開しています。

詳細は↑のREADMEを参照してください。あと、LINE Messaging API側の設定はVoiceflow SDKの記事も参考になると思います。

メインのコードだけ抜粋。

  • あまりエラー処理とかはちゃんとできてないです。
  • LINE Messaging APIの場合は、ユーザごとにuserIdが渡ってくるので、それをState APIのキーにしてます。めっちゃ楽ちんです。
  • 逆にキーとなる情報がない場合(例えばTwilioとかだと、userIdに該当するものが渡ってこないので、ユーザの識別が難しい。発信元の電話番号が使えるかなと思ったけど、非通知だったりする場合もあるので難しい・・・)は、何らかの方法で自前でキーを管理する必要があると思いますし、その場合はStateless APIを使って、Stateを自前で管理するのが良いでしょう。
  • 会話セッションという点では、ほぼ恒久的に残ってる(つまり会話の途中で時間が経ってもそこから再開する)みたいなので、一定時間内に完了させないといけないトランザクションが必要なケース(ショッピングカートとか)では、最後のタイムスタンプ(fetch stateすればわかる)を見て、会話フローの最初からやり直す(type:launchをなげる)ような処理が必要になると思います。
  • Dialog Management APIからはtraceオブジェクトが配列で返ってきます。ここのフォーマットはVoiceflow SDKから変わってるみたい(Audioとかが以前とは違う)で、また、Voice Assistant/Chat Assistantのプロジェクトタイプでも違うみたい。この辺は実際にレスポンスを見たほうがいいと思います。

動作

こんな感じで動きます。

f:id:kun432:20220104015917j:plain

テキストのやり取りはもちろんですが、AudioやVisualでレスポンスが返ってきてるのがわかると思います。

State APIで永続的な情報をVoiceflow側に持たせるとこういうこともできます(2回目以降は名前と年齢を聞いてこない)

f:id:kun432:20220104020252j:plain

その他

Dialog Management APIとはちょっとずれますが、Voice Assistant/Chat Assistantなどのカスタムアシスタント、どうもNLUはMicrosoft LUISを使ってる痛いです。

ビルトインのエンティティタイプ(Alexaでいうスロットタイプのこと)を見てると、それっぽいんですよね。

f:id:kun432:20220104020831p:plain

で、Alexaに比べると種類が少なくて、日本語になると特に少ない。人名などよく使うものがまだ日本語対応されていない(英語と中国語しかない)ので、カスタムでやるしかなく、今回「名前」を受け取りたかったので、テストデータ作成ツールなどを使って、カスタムで5, 600件登録してます。が、それでも認識が悪い。この辺は今後に期待かなと思います。

(Alexaのビルトインスロットのありがたみがよくわかります・・・)

回避策としては以下のようなやり方があるかなーと思います(試してない)

- last_utteranceというユーザの発話・入力をそのまま入れたビルトインの変数があります。スロットに値が入ってない場合はこちらを参照するとよいかも。

- ただし、インテントにマッチしてるかどうかはわからないので、intent_confidenceというインテントへのマッチの信頼度を参考に、一定の信頼度の場合はそれを使う、というような工夫は必要かも

発話・入力の処理が複雑にはなってしまうので、多用はしたくないですね・・・。これについてはそのうちまとめます。

まとめ

次はSlackあたりか、TwiliioでStateless APIを試してみようかなと思います。