Watson Assistant API V1/V2の違い


![image](https://qiita-image-store.s3.amazonaws.com/0/108535/e3305988-0e59-66e2-6718-8f87a8467b5a.png)

# 要は(TL;DR)

- Watson AssistantのAPIにはV1とV2があり、**非互換**部分があります＆機能も違います
- V2 APIはAssistantとSkillをベースにしたアーキテクチャー変更に対応したものであり、**長期的にはV2**を使っていくのがよいです
- V2 APIではAssistantとの会話を司る**Session**というオブジェクトを意識する必要があります
- V2 APIでは**Contextは自動的に維持される**のでV1の時のようにユーザーアプリで毎回送り返す必要はありません
- V2 APIのContextでユーザー変数を使う場合は**"skills"->"main skill"->"user_defined"**（←ここまで決めうち[^1])の下に{"hogevar": "hogevalue"}の形で書く必要があります

[^1]: 「今はきめうち」であってAssistantが複数スキルをサポートしたら変わるかもしれません

# はじめに

こんにちわ！石田です。Assistantマニュアルをよく読まれている方はご存知かと思いますが、Watson Assistantの**APIはV1と新しいV2があります。**この違いについて、あれこれ調べたこと[^2]をお伝えします。**近々でV1 APIが使えなくなるわけではない**ので、皆様が新しいV2のAPIに取り組まれるのは先々でもいいのかな？とは思いますが、新規アプリなどの場合はV2 APIを使っておいたほうがよさそうです。要は先々のお役にたてばと思い記事にします。

- :book: [V1 API Reference](https://console.bluemix.net/apidocs/assistant)
- :book: [V2 API Reference](https://console.bluemix.net/apidocs/assistant-v2)
- :blue_book: APIの説明 [Watson Assistant API Overview](https://console.bluemix.net/docs/services/assistant/api-overview.html#watson-assistant-api-overview)

[^2]: 当記事の元ネタは[このStackOverflowでの質問](https://stackoverflow.com/questions/53404249/watson-assistant-api-v2-manages-the-context-automatically-more-details/53406037)や[このPython_SDKのIssue](https://github.com/watson-developer-cloud/python-sdk/issues/597)です

# API V1とV2の違いサマリー

|  | V1 API | V2 API |
|:---|:---:|:---:|
| ランタイム・メソッド<br>(対話用API) | ○ (/message) | ○ (/message)  |
| オーサリング・メソッド<br>(環境操作API)[^3] |○ | ×(今は、なし) |
| 要求の投げ先 | ワークスペース | アシスタント |
| セッションの利用 | 不可(N/A) | 必須 |
| Contextの維持 | クライアントアプリの責任 | 自動で行われる |
| Contextデータの表現形式 | 任意(key:value) | **"skills"->"main skill"<br>->"user_defined"**の下にkey:valueで書く |
| User-based plansでの<br>user_idセット箇所 | **context.metadata.user_id** | **context.global.system.user_id** |

[^3]: インテントやエンティティを定義する、ダイアログを操作する、などの環境構築系のAPI群



# 概要
![image](https://qiita-image-store.s3.amazonaws.com/0/108535/42b81a40-9875-66b7-3a54-9a48cf5b8c4d.png)

先日、[こちらの記事](https://qiita.com/ishida330/items/666ced65a04243ce286c)でもお伝えしたとおり、2018/11/9にWatson Assistantのアーキテクチャー変更があり、従来のWorkspaceがSkillになり、クライアントとの間にAssistantが介在するようになりました。V2のAPIはこのアーキテクチャーに対応するものです。

![image](https://qiita-image-store.s3.amazonaws.com/0/108535/fdb9956c-2f22-ee2f-8bc5-caf79aacd2a5.png)

上記は新旧APIの道筋を表現した絵です。

- 従来のV1 APIではクライアントはワークスペース(今後はスキル)に対して要求を投げ、直接やり取りをしていました。当然ですが、現時点では従来とおりV1のAPIを使い続けてもOKです。
- 新しいV2 APIを使う場合は要求を投げる先は介在するアシスタントであり、スキルではありません。アシスタントとスキル間のやりとりはクライアントからは見えません。クライアントとアシスタントとのやりとりではV1では存在しなかった**セッション(Session)**というオブジェクトを使います。


# セッションとコンテキストについて
「セッション」はWebアプリケーションではなじみのある用語かと思います。Watson Assistant側の実装はわかりませんが、要はV2 APIで「セッション」ができた事でWatson Assistantのサービス側でセッション上にコンテキストの情報を保持できるようになりました。以前のV1 APIではユーザー変数などのコンテキスト・データを扱いたければ、クライアント側のアプリケーション・ロジックでリクエストの都度(毎回)、全データを受信/(必要なら更新)/再送する必要がありましたが、V2ではその必要はありません。毎回送らなくてもWatson Assistantのサービス側でコンテキストの内容を保持しており、参照できます。以下、新旧対比しつつ、もう少し細かくご説明します。

### 従来の方式(V1 API)
![image](https://qiita-image-store.s3.amazonaws.com/0/108535/8f6e3784-4eb5-361c-724c-8f5b567f0817.png)

V1 APIではWatson Assistantは完全にステートレスなサービスであり、顧客名などのユーザー変数を扱いたい場合はクライアント・プログラムがデータをContextに設定して要求の都度、送信する必要がありました。

V1 APIではContextの形式はJSONで任意のKEY-VALUEペアを書けばいいだけです。例えば以下の形式で要求と共に渡すと、Dialog側では@(myname)で値が参照できます。

```json:例
context={
        'myname': 'ishida'
    }
```
![image](https://qiita-image-store.s3.amazonaws.com/0/108535/338a0bad-d2ea-a6f8-804d-6de55c4eaa70.png)

```json:responseの一部
'output': {'generic': [{'response_type': 'text', 'text': 'hello! Mr.ishida '}], 'text': ['hello! Mr.ishida '],
```

## V2 API
![image](https://qiita-image-store.s3.amazonaws.com/0/108535/622158be-2e92-7a0b-a9f5-9090d82819af.png)

V2 APIではSessionを開始してContextにユーザー変数を設定[^4]してしまえば、あとはSessionをクローズするまで自動的に保持されています。V1の場合のように毎回、全量を送信する必要はありません。

[^4]: Contextの変数はクライアントのアプリケーションで設定することもできますし、Dialogのフローで設定することもできます。

Contextの形式はAPI V2 Rerefenceに定義されていますがV1の時のように好きなkey-valueペアではなく「お約束」があります。具体的にはDialog側で@(myname)で値を参照するには以下のように書かないといけません。
**"skills"->"main skill"->"user_defined"**までは決めうちのキーワードです。(case-sensitiveです) その下に任意のke-valueペアを書きます。別の書き方をするとDialog側で@(myname)で値は参照できません。[^5]

```json:例
context={
        "skills": {
            "main skill": {
                "user_defined": {
                    "myname": "ishida"
                }
            }
        }
    }
```
[^5]: これは「試した結果では、今のSDKではこう書かないと参照できない」というお話です。翻ってAPI_V2のドキュメント上は「Skillsは個々のスキルの固有のデータを保持する」と書いてあります。将来的にAssistantは複数スキルをサポートする予定と聞いていますので、そうなると今のような「決めうちキーワード」の設計では破綻します。よって先々、改めて変更が入るのかもしれません。または私の知らないなんらかの表記のルールがあるのかもしれませんが、ドキュメントに書いてないのでわからないんですよね。。

# 一番シンプルなサンプル・コード(Python)

以下にv1/v2のサンプルコード(Python SDK)をお示しします。（ご参考までに末尾にnode.jsでのサンプルも記載しておきます)　両方を比べると違いが見えてくると思います。

## V1の時
```py3
from watson_developer_cloud import AssistantV1

iam_apikey='xxxxxxxx'
workspace_id = 'xxxxxxxx'
assistant = AssistantV1(
    iam_apikey=iam_apikey,
    version='2018-07-10')

#  message
response = assistant.message(
    workspace_id=workspace_id,
    input={'text':'hello'},
    context={
        'myname': 'ishida'
    }).get_result()
print(response)
```
```json:response
{'intents': [{'intent': 'General_Greetings', 'confidence': 1}], 'entities': [], 'input': {'text': 'hello'}, 'output': {'generic': [{'response_type': 'text', 'text': 'hello! Mr.ishida '}], 'text': ['hello! Mr.ishida '], 'nodes_visited': ['node_13_1502484041694'], 'log_messages': []}, 'context': {'myname': 'ishida', 'conversation_id': 'c5d28afc-6d53-479f-a7cd-ffebc2752729', 'system': {'initialized': True, 'dialog_stack': [{'dialog_node': 'root'}], 'dialog_turn_counter': 1, 'dialog_request_counter': 1, '_node_output_map': {'node_13_1502484041694': {'0': [0]}}, 'branch_exited': True, 'branch_exited_reason': 'completed'}}}
```

## V2の時
V2では、まずセッションを入手してから対話を行う(/message)のが今までとの違いです。

```py3
import json

from watson_developer_cloud import AssistantV2

assistant_id = 'XXXXXXXX'
iam_apikey='XXXXXXXX',

assistant = AssistantV2(
    iam_apikey=iam_apikey,
    version='2018-11-08')
# 【1】Sessionを作る
session_id = assistant.create_session(assistant_id).get_result()
print(json.dumps(session_id, indent=2))

# 【2】Assistant IDとSessionを渡す
message = assistant.message(
    assistant_id,
    session_id["session_id"],
    # 【3】ユーザー変数を参照したい場合は'options':{'return_context': True}
    input={'text':'hello',
        'options':{'return_context': True}
    },
    # 【4】ユーザー変数は必ず"skills"->"main skill"->"user_defined"（←ここまで決めうち)
    #    の下に{"hogevar": "hogevalue"}の形で書く.　この点がV1との違い
    context={
        "skills": {
            "main skill": {
                "user_defined": {
                    "myname": "ishida"
                }
            }
        }
    }).get_result()
print(message)
# 【5】Sessionを削除する
assistant.delete_session(assistant_id, session_id["session_id"]).get_result()

```

```json:response
 {'output': {'generic': [{'response_type': 'text', 'text': 'hello! Mr.ishida '}], 'intents': [{'intent': 'General_Greetings', 'confidence': 1}], 'entities': []}, 'context': {'global': {'system': {'turn_count': 1}}, 'skills': {'main skill': {'user_defined': {'myname': 'ishida'}}}}}
```

【V2のコードの解説】(番号はコメントの番号に対応)
:one: まずはcreate_session()にてSessionを入手します
:two: 以降、/messageで発話する際はアシスタントのID以外に前項で入手したSessionのIDを引き渡します
:three: クライアント側でContextに設定したユーザー変数群を参照したい場合は'options':{'return_context': True}とTrueにします。デフォルトはFalseで、その場合` 'skills': {'main skill': {'user_defined': {'myname': 'ishida'}}}}`の部分はレスポンスに含まれません。
:four: ユーザー変数は必ず**"skills"->"main skill"->"user_defined"**（←ここまで決めうち)の下に{"hogevar": "hogevalue"}のkey-valueペアの形で書きます。上記はCase-Sensitiveであり、間違えるとユーザー変数が参照できません。
:five: 対話が終わったらdelete_session()でセッションを消します

### ご参考) Assistant IDの入手
![image](https://qiita-image-store.s3.amazonaws.com/0/108535/697605e2-c9f5-3f2e-5d4e-842f2e5b40d4.png)

プログラムにセットするAssistant IDは上記から参照できます

# Contextの自動メンテについて

V2 APIではContextにセットしたユーザー変数はWatson Assistantのサービス側にて保持されているのでセッションを消すまで(または会話が無い状態で一定時間[^7]が経過するまで)なくなりません。
以下のコード例では対話の冒頭で'hello'と言う時のみContext=で顧客の名前を渡しています。それ以降は名前をいちいち渡さずとも、顧客の名前が入手できています。（クライアント側でのContextのCRUDも、もちろん可能です)

[^7]: Lire/Standardプランは5分、Plusプランは60分

```py3:contextの自動メンテ
import json
from watson_developer_cloud import AssistantV2

assistant_id = 'xxxxxxxx'
iam_apikey='xxxxxxxx',

assistant = AssistantV2(
    iam_apikey=iam_apikey,
    version='2018-11-08')
session_id = assistant.create_session(assistant_id).get_result()
print(json.dumps(session_id, indent=2))
#########################
# Message
#########################
print('{0}'.format('-'*30))
message = assistant.message(
    assistant_id,
    session_id["session_id"],
    input={'text':'hello',
        'options':{'return_context': True}
    },
    context={
        "skills": {
            "main skill": {
                "user_defined": {
                    "myname": "ishida"
                }
            }
        }
    }
    ).get_result()
print(message)

print('{0}'.format('-'*30))
message = assistant.message(
    assistant_id,
    session_id["session_id"],
    input={'text':'thanks',
        'options':{'return_context': True}
    }
    ).get_result()
print(message)

print('{0}'.format('-'*30))
message = assistant.message(
    assistant_id,
    session_id["session_id"],
    input={'text':'bye',
        'options':{'return_context': True}
    }
    ).get_result()
print(message)

assistant.delete_session(assistant_id, session_id["session_id"]).get_result()

```

```json:response
{
  "session_id": "bbf62560-00d2-4861-9ac7-4b066237ad96"
}
------------------------------
{'output': {'generic': [{'response_type': 'text', 'text': 'hello! Mr.ishida '}], 'intents': [{'intent': 'General_Greetings', 'confidence': 1}], 'entities': []}, 'context': {'global': {'system': {'turn_count': 1}}, 'skills': {'main skill': {'user_defined': {'myname': 'ishida'}}}}}
------------------------------
{'output': {'generic': [{'response_type': 'text', 'text': "You're welcome, Mr.ishida . Just let me know if you need anything else"}], 'intents': [{'intent': 'Thanks', 'confidence': 1}], 'entities': []}, 'context': {'global': {'system': {'turn_count': 2}}, 'skills': {'main skill': {'user_defined': {'myname': 'ishida'}}}}}
------------------------------
{'output': {'generic': [{'response_type': 'text', 'text': 'So long, Mr.ishida '}], 'intents': [{'intent': 'Goodbye', 'confidence': 1}], 'entities': []}, 'context': {'global': {'system': {'turn_count': 3}}, 'skills': {'main skill': {'user_defined': {'myname': 'ishida'}}}}}
```

# user-based planについて
記事「[Watson Assistantの新しいプラン「Plus」と月次ユーザー数課金(MCU)導入のご紹介](https://qiita.com/ishida330/items/86520ffd7b7216d1d471)」に詳細を書きましたが新しいMCU課金への対応に際して、V1/V2 APIで少し違いがあります。よろしければ上記記事もご参照ください。

以上です

## :warning: 注意 2018/11/26現在、V2 APIのドキュメントにはバグがあります

自分がハマッたので書いておきます。（報告したので、そのうち直るでしょうけれど）そもそもはV2 APIでContextを扱う場合、V1の書き方だとユーザー変数が渡らず、悩んでいました。[API V2 Reference](https://console.bluemix.net/apidocs/assistant-v2)を見ましたが「Skillsというエントリーを使うんだよ」という記述以外に細かいContextの書き方が何も書いてませんでしたので「これでどうやれっーつの！」と。それでStackOverflowで聞いたり、とあれこれ調べたら「Curlのタブを選んだ状態で文書を見ると、もっと詳しい情報が表示される」「Java/Node/Pythonのタブを選んだ状態で文書を見ると、何も表示されない」のだと気がつきました。マジか。。

![image](https://qiita-image-store.s3.amazonaws.com/0/108535/59d53516-d436-6149-ee69-dfbac7558a65.png)


## ご参考) node.jsでの一番シンプルなサンプルコード
```js
'use strict';
/*
    Watson Assistant
*/
const express = require('express');
const app = express();
const port = 3000;
var watson = require('watson-developer-cloud');

app.get('/', (req, res) => res.send('Hello World!'));
app.listen(port, () => console.log('Example app listening on port %d', port));


const util = require('util');
var assistant_id = 'XXXXXXXX';
var iam_apikey='XXXXXXXX';

var assistant = new watson.AssistantV2({
  iam_apikey: iam_apikey,
  version: '2018-11-08'
});

async function createSession() {
    var params = {
        assistant_id: assistant_id
    };
    const createSessionPromise = util.promisify(assistant.createSession);
    var response = await createSessionPromise.call(assistant, params);
    return response["session_id"];
}

async function message(session_id, text) {

    var params = {
        assistant_id: assistant_id,
        session_id: session_id,
        input: {
            'message_type': 'text',
            'text': text
        },
        context: {
            "skills": {
                "main skill": {
                    "user_defined": {
                        "myname": "ishida"
                    }
                }
            }
        }
    };
    const messagePromise = util.promisify(assistant.message);
    var response = await messagePromise.call(assistant, params);
    console.log('message response:',
            JSON.stringify(response, null, 2));
}

var session_id; 

async function main() {
    var session_id = await createSession();
    await message(session_id, "Hello");
}

main()

```

```json
Example app listening on port 3000
message response: {
  "output": {
    "generic": [
      {
        "response_type": "text",
        "text": "hello! Mr.ishida "
      }
    ],
    "intents": [
      {
        "intent": "General_Greetings",
        "confidence": 1
      }
    ],
    "entities": []
  }
}
```