【古い記事です】小ネタ - Watson API / Node.jsでコールバック(やPromise)を意識せずAPIを逐次実行するコードを書く方法



:warning: (2019/06/12) 当記事にある**promisify**を使う方法はWatson SDK V3までの古い情報です。2019/3月に出た**SDK V4ではもっと簡単になった**ので、最新のV4を使う予定の方は記事 :newspaper:「 [小ネタ: Watson SDK V4(Node.js)では、もっと楽にコールバック(やPromise)を意識せずAPIを呼び出せるようになりました](https://qiita.com/ishida330/items/6a66477090044fe5701f)」のほうをご参照くださいませ。

---

たとえば「Assistantの応答の確信度が基準値以下の場合は、Discoveryでロングテールの検索を行って回答を提示する」など**複数のWatson APIを同期的に/シーケンシャルに呼び出したい場合**があります。

![image](https://qiita-image-store.s3.amazonaws.com/0/108535/18b07941-76a5-0864-86db-12fce0cf7787.png)

釈迦説ですがNode.js環境は非同期実行の環境なので、こういうことをしたい場合のコードでは**コールバック**とか**Promise**を使う必要があります。でもPromiseって直感的でなくて、**なんか面倒だなあ**と思ってました[^1]。で、StackOverflowで以下のエントリーを発見しました。

[^1]: じゃPythonとか別の言語使えばいいじゃないか、というツッコミもありますが。

:newspaper:[Using Async/Await in WATSON Nodejs SDK](https://stackoverflow.com/questions/49585668/using-async-await-in-watson-nodejs-sdk)
:newspaper:[How can I promisify Watson Assistant functions to allow async/await in node?](https://stackoverflow.com/questions/49859750/how-can-i-promisify-watson-assistant-functions-to-allow-async-await-in-node)

要は**Node.jsのV8からはpromisifyが標準で搭載されて簡単に非同期を意識しないコードが書ける[^2]そうですが、この機構がWatson APIでも利用できます。**例えば以下の感じでWatsonAssistantのインスタンスの”message”APIをpromisifyでくるんでawait付きで呼び出すと、Watson側で処理が完了するまで制御は待ち状態になり、完了してからプログラムに制御が戻る=同期的なコーディングができます。(非同期のややこしいコードが不要になります）

`const messagePromise = util.promisify(assistant.message);`
`var response = await messagePromise.call(assistant, params);`

[^2]: コーディングの観点で同期的なイメージで素直に書ける、というだけであって内部的には非同期/Promiseの機構を使っています。

以下は「Assistantに問いかけて確信度が基準値を下回ったらDiscoveryに改めて問いかける」シナリオの簡単なサンプルですが、同期的な＝わかりやすいコードが実現できているのが見て取れると思います。[^3]

[^3]: 話を簡単にするため、コードはnodejs環境下でjsをバッチ的に実行する形にしています。実際にはWatson_APIの呼び出しはAPサーバー環境下でapp.getやapp.postなどのリクエストを受けた際に使うことになるでしょう。

```js:app.js
'use strict';
/*
    Watson Assistant　& Watson Discoveryをpromisifyを使って逐次実行
*/
/* ibmcloud上のnode.jsで実行するならここをコメントアウト
const express = require('express');
const app = express();
const port = process.env.PORT || 8080;
app.get('/', (req, res) => res.send('Hello World!'));
app.listen(port, () => console.log('Example app listening on port %d', port));
*/
const util = require('util');

console.log('==== start=======');
const assistant_iam_apikey ='xxxxxxxxxxxxxx';
const assistant_url ='https://gateway-syd.watsonplatform.net/assistant/api';
const assistant_workspace_id = 'xxxxxxxxxxxx';

const discovery_iam_apikey ='xxxxxxxxxxxxxx';
const dicovery_iam_url ='https://gateway-syd.watsonplatform.net/discovery/api';
const discovery_collection_id ='xxxxxxxxxxxxxx';
const discovery_configuration_id ='xxxxxxxxxxxxxx';
const discovery_environment_id = 'xxxxxxxxxxxxxx';

var AssistantV1 = require('watson-developer-cloud/assistant/v1');
var assistant = new AssistantV1({
    version: '2018-09-20',
    iam_apikey: assistant_iam_apikey,
    url: assistant_url
});

var DiscoveryV1 = require('watson-developer-cloud/discovery/v1');
const discovery = new DiscoveryV1({
  url: dicovery_iam_url,
  version: '2018-10-15',
  iam_apikey: discovery_iam_apikey
});

// Assistant
async function message(text) {
    console.log("message Start:[" + text + "]")
    var params = {
        input: { text: text },
        workspace_id: assistant_workspace_id
    };
    const messagePromise = util.promisify(assistant.message);
    var response = await messagePromise.call(assistant, params);
    return response;
}

// Discovery
async function query(text) {
    console.log("query Start:[" + text + "]")
    var params = {
        environment_id: discovery_environment_id,
        collection_id: discovery_collection_id,
        passages: true,
        passages_count: 1,
        natural_language_query: text
    };
    const queryPromise = util.promisify(discovery.query);
    var response = await queryPromise.call(discovery, params);
    return response;
}

//***********************************************
// 　Promise/非同期を意識したコードを書かずに済んでます
//***********************************************
async function main() {
    var min_confidence = 0.6; //　求める確信度。これを下回ったらDiscoveryに聞きなおす
    var text = 'Excelで表の必要な部分だけを印刷するにはどうすればいいでしょうか';

    var response = await message(text);
    console.log('confidence of Assistant:',
            JSON.stringify(response.intents[0].confidence, null, 2));
    
    if (response.intents[0].confidence < min_confidence ){
        //　Assistantの回答の確信度が所定の基準を下回ったらDiscoveryにて再度検索
        var response = await query(text);
        console.log('message response from Discovery:',
            JSON.stringify(response.passages, null, 2));        
    } else {
        //　Assistantの回答の確信度が所定の基準以上ならAssistantの回答を提示
        console.log('message response from Assistant:',
            JSON.stringify(response.output.text[0], null, 2));
    }
}

main()

```

以下は実行結果例です

```text:
==== start=======
message Start:[Excelで表の必要な部分だけを印刷するにはどうすればいいでしょうか]
confidence of Assistant: 0.504277801513672
query Start:[Excelで表の必要な部分だけを印刷するにはどうすればいいでしょうか]
message response from Discovery: [
  {
    "document_id": "673b0e32529bf4c614c15acaebb47bab",
    "passage_score": 40.21290429830693,
    "passage_text": "Excelでは、印刷対象を選択して、必要な部分だけを印刷することができます。ここでは
、ご使用のExcelのバージョンに応じた参照先を案内します。\n\n対処方法\n\nExcelで表の必要な部分だけを印
刷する方法については、以下の情報を参照してください。\n※ ご使用のExcelのバージョンに応じた項目をクリ
ックしてください。\n\nExcel 2013の場合\nExcel 2013で表の必要な部分だけを印刷する方法\n\nExcel 2010の
場合\nExcel 2010で表の必要な部分",
    "start_offset": 762,
    "end_offset": 998,
    "field": "text"
  }
]
```

以上です。