情報提供型のチャットボット(Watson Assistant/Functions/Db2連携)で複数件の結果を扱う方法


# 要は(TL;DR;)
- Assistant/Functions/Db2連携で検索結果の件数が複数で不定の場合は、Dialogノード中で[$hoge.joinToArray($template)](https://cloud.ibm.com/docs/services/assistant?topic=assistant-dialog-methods#dialog-methods-arrays-joinToArray)を使うといいよ
- その場合SpELを使うので、Db2の結果セットは辞書型で戻す(Pythonならibm_db.fetch_assocを使う)と便利だよ

# はじめに
チャットボットの典型的な用途の一つに「情報提供」があります。ユーザーから聞かれた質問（例.「この服はいくら？」「この著者の本は？」）に答える類のボットです。Watson Assistantではユーザーからの問いかけへの応答はDialogノードに書かれている文言（テキスト）を返しますが、商品の種類が多い場合、Dialogノード内で個々の商品に関するデータをいちいちベタ書きで保有していては死んでしまうので、通常は商品情報はデータベース等に保有し、Assistantからは条件を与えて検索結果を受け取ったうえでユーザーに回答する形になるはずです。

![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108535/a3a1c6d1-b20e-4e10-36e0-2c01923ee75d.png)

具体的な実装としてはFunctionとDb2[^1]を使うのが定石で、Qiita上でも先達が色々と記事を公開してくれています。

:newspaper: [Watson Assistant から IBM Cloud Functions の Action を呼び出す方法](https://qiita.com/schiyoda/items/bceabe327a0c7d08e9e4)
:newspaper: [Watson AssistantとDb2を外部連係する](https://qiita.com/schiyoda/items/89064693d6353fdc69e1)
:newspaper: [他にもたくさん！](https://qiita.com/search?q=assistant+functions)

![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108535/65318ccc-fd94-10cb-fe4d-72de8b431574.png)

[^1]: DBは別にDb2ではなくてcomposeとかでもいいのですが、要はLitePlanがあるからよく使われているのでしょう。

今回、これらの記事を参考にして某案件のデモを作ったのですが、Db2の**検索結果が複数件で件数が不定の場合**にどう処理するか？にちと悩みましたので、対応方法をシェアさせていただきます。

# シナリオの概要

簡単のためにアクション映画の作品名[^3]と主演男優、作品のWikipediaのURLがDb2テーブルに以下のように9件格納されているものとします。

![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108535/625be989-976b-6781-4371-9bbf2eb649aa.png)

[^3]: この中ではやっぱ「ロッキー」と「燃えよドラゴン」かなあ～。オッサン感満載で恐縮ですが、やっぱ７０年代のはいいですよねエ。Qiitaに書くことではないですが。:sweat:

ユーザーが俳優を指定したら、ボットはその俳優の出演作品をリンク付きで応答します。ここでのポイントは「俳優により出演作の件数が決まっていない」点です。スタローンの場合は4作品ですし、ブルース・リーは１作品です。よってDb2の検索結果の作品件数分、どこかでループしてHTMLを生成する必要があります。

![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108535/5cbd653e-5ff8-3d35-256b-bad7bfee5f12.png)

# シナリオ実装例

:warning: 「例」です。当然ですが他にも「Webアプリ側にデータをそのまま渡して、アプリで動的にHTMLを組み立てる」など色々な方法が考えられます。以下は「あれこれ考えられるけど、これが一番簡単じゃないかなあ」と思う方法のご紹介となります。

### 処理方式
以下のようにAssistant-Functions(Python)-Db2を連携させます。まあ定石かと。
![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108535/7a133513-cbc1-9a13-da1f-0c325b0ee027.png)

- :one: Asistant上のDialogノードから俳優名を引数にしてFunctionsを呼び出します
- :two: Fuctionsのロジックでは俳優名を条件にテーブルを検索して作品リストを入手します。結果は辞書型にしてAssistantに戻します
- :three: AssistantではSpELのテンプレート機能を使って作品リストをHTMLに変換してからアプリに戻します

### 先に全体の見取り図をば

といっても、まあそれなりにややこしいので、先にポイントをサマリーした全体図を下記にお示ししますね。

![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108535/f39bf24f-084d-a131-657a-02710751863c.png)

なおAssistant-Functions-Db2連携の方法は前述の通り[これ](https://qiita.com/schiyoda/items/bceabe327a0c7d08e9e4)とか[これ](https://qiita.com/schiyoda/items/89064693d6353fdc69e1)とか[これ](https://qiita.com/makaishi2/items/349d366e550b37cf4521)などいっぱい記事がありますので割愛し、当記事では「Db2の不定/複数件の結果をHTMLに変換して表示する方法」にフォーカスします。

### AssistantからFunctionsの起動

![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108535/d70f4779-8a27-5a1c-efb8-af252b2837b8.png)

- もしユーザーが「スタローンの映画が見たい」と言ったらインテントは「#MOVIE」、エンティティは「@ACTOR:スタローン」となるようにダイアログ・ノードを設定しました（いわずもがなかもしれませんが、ユーザーが「ブルース・リーの映画が見たい」と言ったらエンティティは「@ACTOR:ブルース・リー」になります）
- 上のノードから以下の内容でFunctionsを起動します（JSON EditorやContext Editorで設定)
![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108535/f4e080fb-fc2d-409d-3738-f8ac2c84dabc.png)


```json:Function起動
{
  "output": {
    "text": {
      "values": [],
      "selection_policy": "sequential"
    }
  },
  "actions": [
    {
      "name": "/xxxxx_yyy/SELECT_MOVIES",
      "type": "server",
      "parameters": {
        "ACTOR": "@ACTOR.literal"
      },
      "credentials": "$icf_cred",
      "result_variable": "context.titles"
    }
  ],
  "context": {
    "icf_cred": {
      "user": "5bd76439-4df0-4963-85b0-xxxxxxxxxxxx",
      "password": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    }
  }
}
```

設定上のポイント

- name: 起動するFunctionsの名前
- parameters: エンティティ@ACTORの値(literal)が「スタローン」なので辞書型で{"ACTOR":"スタローン"}が引数として渡される
- result_variable: Functionsからの戻り値（辞書型)の名前。今回はcontext.titlesと名前を付けたので、子ノード(「映画のリストを表示」)のSpEL中では<? $titles ?>で参照できる

### Functionsでの処理

以下のPythonロジックをFunctionsとして定義します[^2]

[^2]: 接続文字列やスキーマ、テーブル/カラム名は適宜変更してください

<details><summary>参考) FunctionsはGUIだけでなくコマンド(CLI)でも定義できます</summary><div>

- :blue_book:  [スタンドアロン IBM Cloud CLI のインストール](https://cloud.ibm.com/docs/cli/reference/ibmcloud?topic=cloud-cli-install-ibmcloud-cli)
- :blue_book: [BM Cloud Functions CLI プラグインのインストール](https://cloud.ibm.com/functions/learn/cli)
- :blue_book: [IBM Cloud Functions CLI reference](https://cloud.ibm.com/docs/cloud-functions-cli-plugin?topic=cloud-functions-cli-plugin-functions-cli)

```text:定義(create/update)例
ibmcloud fn action delete SELECT_MOVIES
ibmcloud fn action create SELECT_MOVIES SELECT_MOVIES.py --kind python:3.7 
または
ibmcloud fn action update SELECT_MOVIES SELECT_MOVIES.py --kind python:3.7
```
:warning:  --kind python:3.6 or 3.7付けないとデフォルトがpython:2なので注意！

```text:起動確認例
ibmcloud fn action invoke --result SELECT_MOVIES --param "ACTOR" "ブルース・リー"
```
:point_up:  -d を付けるとデバッグ情報が出力します。うまく動かない時の問題判別に便利です。

</div></details>


```py:SELECT_MOVIES.py
#
#
# main() このアクションを呼び出すときに実行されます
#
# @param Cloud Functions アクションは 1 つのパラメーターを受け入れます。このパラメーターは JSON オブジェクトでなければなりません。
#
# @return このアクションの出力。この出力は、JSON オブジェクトでなければなりません。
#
#
import sys
import ibm_db

def main(dict):
    rows = []
    conn = ibm_db.connect("DATABASE=BLUDB;HOSTNAME=dashdb-txn-sbox-yp-dal09-04.services.dal.bluemix.net;PORT=50000;PROTOCOL=TCPIP;UID=xxxxxxx;PWD=xxxxxxxxxxx;","","")
    sql = "SELECT TITLE,URL FROM xxxxxxx.ACTION_MOVIE WHERE ACTOR='{}'".format(dict["ACTOR"])
    stmt = ibm_db.exec_immediate(conn, sql)
    row = ibm_db.fetch_assoc(stmt)
    while (row):
        rows.append(row)
        row = ibm_db.fetch_assoc(stmt)
    result = {'result': rows}
    return result
```

ポイント

- Db2/Python(ibm_db) で結果セットから行または列をフェッチする関数は[fetch_tuple/fetch_assoc/fetch_both/fetch_rowの4通り](https://www.ibm.com/support/knowledgecenter/ja/SSEPGG_11.5.0/com.ibm.swg.im.dbclient.python.doc/doc/t0054388.html)ありますが、

>ibm_db.fetch_assoc 	列名によって索引付けされた、結果セット内の 1 行を表すディクショナリーを戻します。

ので、**fetch_assoc**を使いましょう。というのもSpELは辞書型と相性がいいからです。結果として以下のデータがAssistantに戻ります。

```js:ibm_db.fetch_assocでの結果
{  
   'result':[  
      {  
         'TITLE':'ロッキー',
         'URL':'https://ja.wikipedia.org/wiki/%E3%83%AD%E3%83%83%E3%82%AD%E3%83%BC_(%E6%98%A0%E7%94%BB)'
      },
      {  
         'TITLE':'エクスペンダブルズ',
         'URL':'https://ja.wikipedia.org/wiki/%E3%82%A8%E3%82%AF%E3%82%B9%E3%83%9A%E3%83%B3%E3%83%80%E3%83%96%E3%83%AB%E3%82%BA'
      },
      {  
         'TITLE':'ランボー',
         'URL':'https://ja.wikipedia.org/wiki/%E3%83%A9%E3%83%B3%E3%83%9C%E3%83%BC'
      },
      {  
         'TITLE':'大脱出',
         'URL':'https://ja.wikipedia.org/wiki/%E5%A4%A7%E8%84%B1%E5%87%BA'
      }
   ]
}
```

Assistant側ではFunctionの戻り値("result_variable")を "context.titles"としたので、戻った後のAssistant上でのSpELでは、例えば以下のような表記でデータにアクセスすることができます。

|表現例|意味|
|:---|:---|
|<? $titles.result ?>| TITLEとURLのペア×4行分の配列|
|<? $titles.result[0].TITLE ?>| "ロッキー"|
|<? $titles.result[1].TITLE ?>| "エクスペンダブルズ"|

<details><summary>参考:Db2でそれ以外の関数を使った場合</summary><div>

【1】ibm_db.**fetch_tuple**の場合は以下のようにデータだけが返ります。

```json:ibm_db.fetch_tuple
{
   'result':[
      ('ロッキー',
      'https://ja.wikipedia.org/wiki/%E3%83%AD%E3%83%83%E3%82%AD%E3%83%BC_(%E6%98%A0%E7%94%BB)'      ),
      ('エクスペンダブルズ',
      'https://ja.wikipedia.org/wiki/%E3%82%A8%E3%82%AF%E3%82%B9%E3%83%9A%E3%83%B3%E3%83%80%E3%83%96%E3%83%AB%E3%82%BA'      ),
      ('ランボー',
      'https://ja.wikipedia.org/wiki/%E3%83%A9%E3%83%B3%E3%83%9C%E3%83%BC'      ),
      ('大脱出',
      'https://ja.wikipedia.org/wiki/%E5%A4%A7%E8%84%B1%E5%87%BA'      )
   ]
}
```

よって、以下のように自分で辞書型に組み立ててもいいです（あえてそうする理由もありませんが）

```py:抜粋
    stmt = ibm_db.exec_immediate(conn, sql)
    row = ibm_db.fetch_tuple(stmt)
    while (row):
        d = {"TITLE": row[0], "URL": row[1]}
        rows.append(d)
        row = ibm_db.fetch_tuple(stmt)
    result = {'result': rows}
```

【2】ibm_db.**fetch_both**の場合は以下のように両方の形式で返ります

```json:ibm_db.fetch_both
{
   'result':[
      {
         'TITLE':'ロッキー',
         0:'ロッキー',
         'URL':'https://ja.wikipedia.org/wiki/%E3%83%AD%E3%83%83%E3%82%AD%E3%83%BC_(%E6%98%A0%E7%94%BB)',
         1:'https://ja.wikipedia.org/wiki/%E3%83%AD%E3%83%83%E3%82%AD%E3%83%BC_(%E6%98%A0%E7%94%BB)'
      },
      {
         'TITLE':'エクスペンダブルズ',
         0:'エクスペンダブルズ',
         'URL':'https://ja.wikipedia.org/wiki/%E3%82%A8%E3%82%AF%E3%82%B9%E3%83%9A%E3%83%B3%E3%83%80%E3%83%96%E3%83%AB%E3%82%BA',
         1:'https://ja.wikipedia.org/wiki/%E3%82%A8%E3%82%AF%E3%82%B9%E3%83%9A%E3%83%B3%E3%83%80%E3%83%96%E3%83%AB%E3%82%BA'
      },
      {
         'TITLE':'ランボー',
         0:'ランボー',
         'URL':'https://ja.wikipedia.org/wiki/%E3%83%A9%E3%83%B3%E3%83%9C%E3%83%BC',
         1:'https://ja.wikipedia.org/wiki/%E3%83%A9%E3%83%B3%E3%83%9C%E3%83%BC'
      },
      {
         'TITLE':'大脱出',
         0:'大脱出',
         'URL':'https://ja.wikipedia.org/wiki/%E5%A4%A7%E8%84%B1%E5%87%BA',
         1:'https://ja.wikipedia.org/wiki/%E5%A4%A7%E8%84%B1%E5%87%BA'
      }
   ]
}
```

【3】ibm_db.**fetch_row**は複雑でめったに使わないので割愛

</div></details>


### Functions戻り後のAssistant側での処理


FunctionsからJSON(ディクショナリー)型で作品リストが入手できたので、SpELで件数分、HTMLに変換します。SpELにはループの機構がありませんが、[JSONArray.joinToArray(template)](https://cloud.ibm.com/docs/services/assistant?topic=assistant-dialog-methods#dialog-methods-arrays-joinToArray)で各要素にテンプレートを適用しつつ順に処理できますので、それを活用します。

具体的にはFunctionからの戻りである下記のようなjsonに対して

```json:context.titles(簡単のため整形・省略してます)
{'result':[  
      {'TITLE':'ロッキー', 'URL':'https://ja.wikipedia.org/wiki/XXXX'},
      {'TITLE':'エクスペンダブルズ','URL':'https://ja.wikipedia.org/wiki/XXXX'},
      {'TITLE':'ランボー','URL':'https://ja.wikipedia.org/wiki/XXXX'},
      {'TITLE':'大脱出','URL':'https://ja.wikipedia.org/wiki/XXXX'}
]}
```
子ノードでテンプレートとなる変数 $TEMPLATEを定義します(名前は任意) 

![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108535/c97e2bdc-0232-0d35-2435-366b870ac2a7.png)

```html:$TEMPLATEのVALUE
<li><a href="%e.URL%" target="_blank">%e.TITLE%</a></li>
```
:point_up: ちなみに上記のように素のhtmlを書いて保存したら、ちゃんと以下のようにEscapeされてました。

```html:$TEMPLATEのVALUE-エスケープ後
"<li><a href=\"%e.URL%\" target=\"_blank\">%e.TITLE%</a></li>"
```


VALUEで表示用のHTMLのスニペットを定義しますが、**%e.キー名%**で配列中の一要素を表現できます(eは固定値で変更できません)

![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108535/b1c6dcf1-fa5a-0cbb-577e-e776e2d27e30.png)

And respond withではテキストレスポンスを定義し、[JSONArray.joinToArray(template)](https://cloud.ibm.com/docs/services/assistant?topic=assistant-dialog-methods#dialog-methods-arrays-joinToArray)で複数の要素にひとつずつテンプレートを適用していきます。

```js
<?  @ACTOR.literal?>の出演映画は<? $titles.result.joinToArray($TEMPLATE).join(" ") ?>です
```

この辺の詳細はドキュメント[JSONArray.joinToArray(template)](https://cloud.ibm.com/docs/services/assistant?topic=assistant-dialog-methods#dialog-methods-arrays-joinToArray)の「Compex template」に説明があります。
![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108535/2098ca35-ee5d-6c98-01c7-a6a341b27d3b.png)

ということで、最終的に冒頭でお示ししたように情報提供型のチャットボットで外部テーブルを参照して不定/複数件の結果を表示することができました。この方法だとAssistantの段階でHTMLになっているので「Try it out」で簡単に確認できるのもいいところだと思います。
![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108535/8c06ce40-a69b-8a3a-ca23-dbe0511d7252.png)

以上で「勘所」をご紹介しましたが、色々な設定が結びついているので、改めて[全体の見取り図](#先に全体の見取り図をば)を眺めてみてください。今度は「ああ、そういうことか」とご納得いただけるかと思います。

# ご参考
ご自身でも手早くお試しいただけるように、リソース一式、[githubのここ](https://github.com/ishida330/WatsonAssistant-Funtions-Sample)に置いておきますね。