ServiceNowでWatson Assistantを使ってChatOpsを実現する

# 概要

ChatOpsとはChatによってシステム運用を行う仕組みを指す。
Chatの相手先は人ではなく、ChatBotを使用する。 ChatBotを使ってシステム運用を自動化することにより、システム運用の効率化、コスト削減、レスポンス向上の実現が可能だ。例えばシステムの状態を監視し、負荷が高い場合に自動的にChatBotからシステム運用者にチャットで状況報告するとともに、クラウドのCPUの追加、メモリの追加などの対応の選択肢をChatBotがシステム運用者とのチャットにより聞き出し、ChatBotがCPUの追加、メモリの追加を自動的に行ったり、ユーザーからのシステムの利用状況の問い合わせに対して、ChatBotが利用状況の調査結果をユーザーに回答したりといたことが実現できる。
ServiceNowにはチャット機能が用意されているが、ChatOpsのための専用の仕組みはKINGSTONEではまだ用意されていない。
そこで、IBMのWatson Assistantと連携することにより、ChatOpsを実現する。
既に、ServiceNowでボットチャットを作成する方法、Watson Assistantで対話(Dialog)を作成する方法、ServiceNowのOutbound IntegrationsのREST Messageを使ってWatson AssistantのMessage APIを起動する方法については[こちらで](https://qiita.com/sukkyxp/items/72dcf6742636df8f1840)解説済みだ。
今回は、システム運用者がユーザーまたは部門の追加をChatBotに依頼して、ChatBotがユーザーまたは部門のアカウント追加を行う仕組みを作ってみた。

## 参考

[ServiceNowでチャットボットを作成する（オーム返し版）](https://qiita.com/sukkyxp/items/76105f0dc11a5660b869)
[Watson Assistantでユーザーまたは部門を追加するチャットボットを作成する](https://qiita.com/sukkyxp/items/a3375382f17bff8556af)
[ServiceNowのOutbound REST MessageでWatson Assistant APIを起動する](https://qiita.com/sukkyxp/items/72dcf6742636df8f1840)

## 環境

ServiceNow : KINGSTONE
Watson Assistant API : 2018-02-16

## 前提条件

上記参考資料で作成したWatson AssistantのWorkspace、ServiceNowのOutbound IntegrationsのREST Messsage、オーム返しボット用のBusiness Rule、Script Includeを作成済みであること。

#手順
## コンテキスト保持テーブルの作成

Watson AssistantのMessage APIはユーザー入力のメッセージを送信すると、Watson Assistantの応答メッセージと共に、対話を持続するためのコンテキスト情報が一緒に応答メッセージとして返ってくる。次回以降のユーザーのメッセージ送信時は、このコンテキスト情報をつけて送信する必要がある。このコンテキスト情報を保持するために、ユーザーのsysIDをキーとしてコンテキストを保持するテーブルを作成する。

|属性|値|
|---|---|
|Label|watson_conversation|
|Name|x_211750_angular_a_watson_conversation|

NameはLabelの入力値を元にシステムによって自動生成される。
後ほど、GlideRecordでテーブルを開く場合は、ここで生成されたNameを使用するので控えておく。

また、テーブルに下記の２つの列を追加する。

|Column Label|Column Name|型|最大長|
|---|---|---|---|
|Profile | profile|String(Full UTF-8)|128|
|Context | context|String(Full UTF-8)|4000|

Context列は大きめに確保している。

##Script Includeの作成

参考資料で作成したScript Includeに関数を追加し下記のScriptを設定する。

```javascript
var LiveChatUtility = Class.create();
LiveChatUtility.prototype = {
	initialize: function() {
	},
	getProfileFromName:function(name) {
		var gr = new GlideRecord('live_profile');
		gr.addQuery('name', name);
		gr.query();
		if(gr.next()) {
			return gr.getUniqueValue();
		}
	},
	isLiveGroupMember:function(group, profile) {
		var gr = new GlideRecord('live_group_member');
		gr.addQuery('group', group);
		gr.addQuery('member.name', profile);
		
		gr.query();
		return gr.next();
		
	},
	hasContext:function(profile) {
		var gr = new GlideRecord('x_211750_angular_a_watson_conversation');
		gr.addQuery('profile', profile);
		
		gr.query();
		return gr.next();
	},
	getContext:function(profile) {
		var gr = new GlideRecord('x_211750_angular_a_watson_conversation');
		gr.addQuery('profile', profile);
		
		gr.query();
		if (gr.next()) {
			return gr.getValue('context');
		}
	},
	insertContext:function(profile, context) {
		gs.info('context追加処理開始');
		gs.info('insert profile = ' + profile);
		gs.info('insert context = ' + context);
		try {
			// 追加
			var gr = new GlideRecord('x_211750_angular_a_watson_conversation');
			gr.initialize();
			gr.setValue('profile', profile);
			gr.setValue('context', context);
			gr.insert();
		}
		catch(ex) {
			gs.error(ex.getMessage());
		}
	},
	updateContext:function(profile, context) {
		gs.info('context 更新処理開始');
		
		try {
			// 更新
			var gr = new GlideRecord('x_211750_angular_a_watson_conversation');
			gr.addQuery('profile', profile);
			gr.query();
			if (gr.next()) {
				gr.setValue('context', context);
				gr.update();
			}
		}
		catch(ex) {
			gs.error(ex.getMessage());
		}
	},
	addUserOrGroup:function(context) {
		var addtarget = context.addtarget;
		var objectname = context.objectname;
		
		gs.info('addUserOrGroup:addtarget = ' + addtarget);
		gs.info('addUserOrGroup:objectname = ' + objectname);
		
		if ('user' == addtarget) {
			this.addUser(objectname);
	
		} else if ('group' == addtarget) {
			this.addGroup(objectname);
		}
	},
	addUser:function(username) {
		var gr = new GlideRecord('sys_user');
		gr.initialize();
		gr.first_name = username;
		gr.name = username;
		gr.insert();
		
	},
	addGroup:function(groupname) {
		var gr = new GlideRecord('sys_user_group');
		gr.initialize();
		gr.name = groupname;
		gr.insert();
	},
	
	
	type: 'LiveChatUtility'
};
```

今回追加した関数を説明していく。

```javascript
hasContext:function(profile)
```

引数で指定されたユーザープロファイル（ユーザーのsysID）をキーとするレコードが「watson_conversation」テーブルに存在すればtrueを返却する。

```javascript
getContext:function(profile)
```
引数で指定されたユーザープロファイル（ユーザーのsysID）をキーとするwatson_conversationテーブルのレコードのコンテキスト情報を返す。

```javascript
insertContext:function(profile, context)
```

引数で指定されたユーザープロファイル（ユーザーのsysID）とコンテキストのレコードをwatson_conversationに追加する。


```javascript
updateContext:function(profile, context)
```

引数で指定されたユーザープロファイル（ユーザーのsysID）をキーとするwatson_conversationテーブルのレコードのコンテキスト情報を引数のcontextで更新する。

```javascript
addUserOrGroup:function(context)
```

このコードは今回の最も重要な部分だ。

```javascript
var addtarget = context.addtarget;
var objectname = context.objectname;
```

このコードでcontextからaddtargetとobjectnameを取得している。
このaddtargetとobjectnameをどこで設定しているかと言うと、Watson AssistantのDialogの設定だ。参考資料で設定内容を確認して欲しい。

addUser、addGroupは、それぞれsys_userテーブル、sys_user_groupテーブルにユーザー、部門を追加する。なお、それぞれのテーブルはスクリプトからのレコードCreateの許可をオンにしておく必要がある。
Dialogの設定でWatson Assistantが認識したIntentとEntityを元にコンテキストに値を設定している。このコンテキストの値を元にユーザーまたは部門の追加をスクリプトで実施する。

##Business Rule（チャット自動応答スクリプト）の拡張

```javascript
(function executeRule(current, previous /*null when async*/) {
	var userDislayName = gs.getUserDisplayName();
	
	var chatUtil = new LiveChatUtility();
	
	var currentUserProfile = chatUtil.getProfileFromName(userDislayName);
		
	var profilefromrecord = current.getValue('profile');
	
	if (currentUserProfile != profilefromrecord) {
		gs.info('プロファイルが現在のユーザーで無いため処理を抜けた。');
		return;
	}
	
	// Live Messageのgroupidを取得
	var convId = current.getValue('group');
	
	var CHATBOTUSERNAME = 'Service Desk';
	
	// BOTのprofileを取得
	var botprofile = chatUtil.getProfileFromName(CHATBOTUSERNAME);	
	if (chatUtil.isLiveGroupMember(convId, CHATBOTUSERNAME) == false) {
		gs.info('現在のチャットグループにボットユーザーが含まれないために処理を抜けた。');
		return;
	}
	
	gs.info('チャットボット 自動応答処理開始...');
	
	var message = current.getValue('message');
	
	
	var context = '{}';
	
	var hasContext = false;
	if (chatUtil.hasContext(currentUserProfile) == true) {
		hasContext = true;
		context = chatUtil.getContext(currentUserProfile);
	}
	
	var watson = new WatsonAssistantApi();
	var response = watson.send(message, context);
	
	var lm = new global.LiveFeedMessage();
	var data = {
		"message": response.output.text,
		"group_id": convId,
		"from_profile":botprofile
	};
	lm.postMessage(data);
	
	chatUtil.addUserOrGroup(response.context);
	
	var parser = new global.JSON();
	var contextstring = parser.encode(response.context);
	if (hasContext == true) {
		chatUtil.updateContext(currentUserProfile, contextstring);
	} else {
		chatUtil.insertContext(currentUserProfile, contextstring);
	}
	
	gs.info('自動応答処理完了.');
	
})(current, previous);
```

今回はコンテキストをRest MESSAGEの応答から取得し、「watson_conversation」テーブルにユーザープロファイル(sysID)をキーとして保存または取得する処理と、
Watson AssistantのRest MESSAGEにユーザーの入力メッセージとコンテキストを設定して送信処理を追加している。

```javascript
var watson = new WatsonAssistantApi();
var response = watson.send(message, context);
```
これは参考資料で作成したOutbound IntegrationsのREST Messageを起動するためのScript Includeだ。次の節で説明する。

##REST Message起動用のScript Includeの作成

「WatsonAssistantApi」の名前でScript Includeでを作成しScriptに下記を入力する。

```javascript
var WatsonAssistantApi = Class.create();
WatsonAssistantApi.prototype = {
	initialize: function() {
	},
	send:function(message, context){
		
		gs.info('Watson Assistant起動処理開始...');
		
		try {
			var r = new sn_ws.RESTMessageV2('x_211750_angular_a.WatsonAssistant', 'Message');
			
			gs.info('message = ' + message);
			gs.info('context = ' + context);
			r.setStringParameterNoEscape('context', context);
			r.setStringParameterNoEscape('message', message);
			
	
			
			var response = r.execute();
			var responseBody = response.getBody();
			var httpStatus = response.getStatusCode();
			
			gs.info('Watson Assistant http status = ' + httpStatus);
			gs.info('Watson Assistant responseBody = ' + responseBody);
			
			
			var parser = new global.JSON();
			var parsed = parser.decode(responseBody);
			
			return parsed;
		}
		catch(ex) {
			gs.error(ex.getMessage());
		}
	},
	
	type: 'WatsonAssistantApi'
};
```

#動作確認

ServiceNowのブラウザに「chat」と入力してエンターでチャットを起動する。
Service Desk（チャットボット用ユーザー）に対してチャットを開始し、下記の通り入力する。

![adduserdialog.png](https://qiita-image-store.s3.amazonaws.com/0/197947/a061e0fb-695a-0f89-3012-47bfcfa17b7b.png)

sys_userテーブルを開いて、userhogeが追加されていることが確認できる。

![userhoge.png](https://qiita-image-store.s3.amazonaws.com/0/197947/255eb6ce-37ce-22d1-3c84-97e69d03f515.png)

##コンテキスト保持テーブル(watson_conversation)の確認

![watson_conversation_record.png](https://qiita-image-store.s3.amazonaws.com/0/197947/c512a71e-19ea-33bd-88eb-2c707c3ac86d.png)

コンテキストの内容は下記の通り。

```json

{
    "addtarget": "group",
    "conversation_id": "a8a957b2-6bfd-4df2-881e-2a73f9e8c7c2",
    "objectname": "g1010101",
    "system": {
        "_node_output_map": {
            "node_11_1524953286671": [
                0
            ],
            "node_4_1524918503718": [
                0
            ],
            "node_5_1524918589117": [
                0
            ],
            "node_5_1524951184739": [
                0,
                0
            ]
        },
        "branch_exited": true,
        "branch_exited_reason": "completed",
        "dialog_request_counter": 6,
        "dialog_stack": [
            {
                "dialog_node": "root"
            }
        ],
        "dialog_turn_counter": 6
    }
}
```


##Watson Assistantの設定が不十分な点について

Watson Assistantの設定が不十分なため、「ユーザー名を入力してください」の後に、「追加」を入力すると「追加」ユーザーが追加されるという変な動作となる。
今回はWatson AssistantとServiceNowのチャットを連携する方法の説明に主眼を置いているため、Watson Assistantの設定については、不十分なままでとりあえずは良しとしている。実際に実用にするにはさらに複雑な設定が必要になるだろう。

# 最後に

Watson Assistantを使ったチャットボット機能は、この他にもユーザーに問い合わせに関する回答実施といった使い方や、またユーザーの問い合わせについて、チャットボット自身が回答を見つけれない場合は、サポートデスク担当者に対してチャットボットが会話を開始し、サポートデスク担当者から回答を得て、ユーザーに回答するといった使い方も考えられる。