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


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


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

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

Using Async/Await in WATSON Nodejs SDK
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);

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

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()

以下は実行結果例です

==== 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"
  }
]

以上です。


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

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

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