Watson AssistantからIBM Cloud functionsを使う時の考慮点

## 初めに
本記事は、[開発相談会#4 : IBM CloudFunction/Laravel](https://bmxug.connpass.com/event/100749/)でのディスカッション内容です。

## サンプルアプリケーションについて
[こちら](https://qiita.com/motuo/items/510b499c19d0c5c7be3e)で作成した、LaravelとWatson Assistantを連携したアプリケーションを使って検証をしています。
ソースコードは[Github](https://github.com/motuo1201/Watson_Caller)で公開しています。

## Watson AssistantからIBM Cloud Functionsを使うには…
使うだけであれば簡単です。
[公式のドキュメント](https://cloud.ibm.com/docs/assistant?topic=assistant-dialog-webhooksの力を借りることで、かなり簡単にWatson AssistantからIBM Cloud Functionsを使うことが出来るようになりました。

## 実際に使ってみると…
ただ、実際にユーザーアプリケーションからWatson Assistantを呼び出すにあたって、疑問:question:ポイントがありました。

### IBM Cloud Functionsのキーを持たせる場所
IBM Cloud Functionsを呼び出す際には、`credentials`というパラメータで、`user`と`password`を渡す必要があります。

下記の様に実際に呼び出すNodeでAPIキーを設定して呼び出すこともできますが、公式ドキュメントにも記載がある通り、コンテキストの一部として資格情報を渡すのが正解のようです。

```
資格情報を保護するために、資格情報は Watson Assistant ワークスペースに保管しないでください。
代わりに、コンテキストの一部としてクライアント・アプリケーションから渡してください。
メッセージ・コンテキストの $private セクション内にコンテキスト変数をネストすると、
この情報が Watson ログに保管されることを防止できます (例: `$private.my_credentials`)。
```

(contextをNodeで直接設定する方法)←**本番でこれはNG:no_good:**

```json
{
  "context": {
    "private": {
      "my_credentials": {
        "user": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "password": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
      }
    }
  },
  "output": {
    "generic": [
      {
        "values": [
          {
            "text": "どこの天気を知りたいですか？"
          }
        ],
        "response_type": "text",
        "selection_policy": "sequential"
      }
    ]
  }
}
```

#### IBM Cloud Functionsを呼び出すNodeの書き方

```json
{
  "output": {
    "generic": [
      {
        "values": [
          {
            "text": ""
          }
        ],
        "response_type": "text",
        "selection_policy": "sequential"
      }
    ]
  },
  "actions": [
    {
      "name": "/enomotuProject_testSpace/getWeather",
      "type": "server",
      "parameters": {
        "location": "$repeat"
      },
      "credentials": "$private.my_credentials",
      "result_variable": "context.weather"
    }
  ]
}
```

#### PHPから呼び出すときのSample

```php
<?php
namespace App\Services;

use GuzzleHttp\Client;

class CallWatsonAssistant{
    /**
     * Watson Assistantを呼び出すモジュール
     *
     * @param string $spokenWord ユーザーが入力した文字列
     * @param array $context watson assistantのcontextデータ
     * @return json Watson AssistantをCallした結果
     */
    public function call(string $spokenWord,array $context)
    {
        $context["private"] = ["my_credentials" => 
            [
                //ここは環境変数から取得
                "user"     => config('watson.icf_user'),
                "password" => config('watson.icf_password')
            ]
        ];

        $requestData  = json_encode(['input'=>['text'=>$spokenWord],'context'=>$context]);
        $headers = ['Content-Type' => 'application/json','Content-Length' => strlen($requestData)];
        $curlOpts = [
            CURLOPT_USERPWD        => config('watson.user_name').':'.config('watson.password'),
            CURLOPT_POSTFIELDS     => $requestData
        ];
        $path         = config('watson.workspace_id') . '/message?version=2018-07-10';
        $guzzleClient = new Client(['base_uri'=>'https://gateway-fra.watsonplatform.net/assistant/api/v1/workspaces/']);
        return $guzzleClient->request('POST',$path,['headers'=> $headers,'curl'=>$curlOpts])->getBody()->getContents();
    }
}
```

**ただ、このままですとREST APIのレスポンスに`IBM Cloud Functions`のcontextにAPIキーが埋め込まれてしまう(下図参照)**のでWebアプリから使う時には何らかの工夫が必要だと思います。
![Request.png](https://qiita-image-store.s3.amazonaws.com/0/154110/26b22b41-3d99-3591-6668-5115f7054106.png)

私は下記の様に`private`contextをクリアするNodeを持たせて、全てのNodeの最後はここに`Jump to`するようにしています。
ただ、必ずJumptoを仕込む手間が面倒なのと、この`Jump to`を忘れた時にAPIキーが外に出る恐れがあるので、より良い方法を模索中です:sweat_smile:
![CleaCredentials.png](https://qiita-image-store.s3.amazonaws.com/0/154110/4741f7af-10a5-963c-8488-5c79a08e0132.png)


### エラーハンドリングはどうするか？
何も判定していない状態では、ありえない住所を入力した場合に下記のようになってしまします。
![5.PNG](https://qiita-image-store.s3.amazonaws.com/0/154110/957e497d-7795-4892-bf5b-37dfb286c533.png)

ただ、実際のWebアプリケーションに返る値は正常値で、上記の例ですと「ほげほげの天気は」で文章が止まったままとなってしましまいます。
理想は「入力された地域が見つかりません」となることだと思いますが、このエラーハンドリングも掘り下げて考える必要があります。

#### CASE1 資格情報などが誤っており呼び出しに失敗する。
この場合は、[公式ドキュメント](https://console.bluemix.net/docs/services/conversation/dialog-actions.html#-)にある通り、`context`に`cloud_functions_call_error`が設定されます。
この時にはエラー内容をNodeに返してしまっても良いかもしれません。
![ErrorSample1.PNG](https://qiita-image-store.s3.amazonaws.com/0/154110/28bfd451-7a3b-2908-924f-cbf1e1b4a4f3.png)

#### CASE2 呼び出し自体には成功したがICF側でエラーとなった場合
様々な原因が考えられますが、可能な限りでIBM Cloud Function側でエラーの原因やメッセージなどをセットして返せると良いのでは、と思います。
というのも、ICF側で502等のステータスとなった場合には、returnしたcontextには`error`という値のみが入ってくることになる為です。
ここも気を付ける点になりそうです。

### おまけと宣伝
上記とは無関係ですが、勉強会の当日にはIBM様や@tokidaさんからIBM Cloud Functionに関する興味深い話を聞くことが出来ました。
箇条書きではありますが、IBM Cloud Functionsについて下記のメリットについて細かく聞くことが出来ました。

1. 応答時間はCloud Foundryとほぼ同じ応答時間です！
2. IBM Cloud FunctionsのSLAは99.95%です！
3. dockerを使えばどのような言語でもサービスでも実装OK
4. 必ずしもマイクロサービス的な使い方をする必要はない…が、1つのFunctionは60sまでしか動かないので注意。
5. 最初からLog Analysysサービスと統合されているのですぐにログ管理ができる！
6. 今のシステムの隙間を埋めるような使い方が可能。  
    Apache Open Whiskはログ管理機能がない(IBM Cloud Functionsの優位性はここにある)

IBM Cloudを使ってみたい(もしくは実際に使った)けど、どう使ったらよいか分からない！という方は、是非、相談会に遊びに来てください。
次回の開催情報は[こちら](https://bmxug.connpass.com/event/103839/)です。