Vancouverリリースから追加されたNow Assistのいくつかの機能1に触れる機会があったのですが、Now Assist利用で発生する追加コスト2の負担がちょっと厳しいなと思いました。
そこでOpenAIやAzure OpenAIサービスのどちらもREST APIに対応していますし自分で作ったほうがいいかなと思い、ServiceNow→ChatGPTへの処理を行う仕組みを作ってみました。
また、将来的なメンテナンス性や拡張性を考慮してChatGPTへのプロンプトの内容をスクリプト内にハードコートはせず、ノーコードで追加・更新・削除できるような形での作成を行っていきます。
全体図
全体図としては以下のコンポーネントで構成されたものになります。
1. 拡張テーブル:REST API接続の各種パラメーターを格納・管理する
2. Script Include:REST API接続を行う
3. Service Catalog(Catalog item):エンドユーザーが利用する
ChatGPTの準備
まずはともあれChatGPT側の環境が準備されていないことには何もできないのでREST API接続できる環境を準備します。
(多分他の記事でたくさん書かれているので)詳細は省きますが、個人でまず試してみたいという方は準備のハードルが低い3OpenAIのサービスを利用されるのが良いかと思います。
※この記事ではAzure OpenAIサービスを使用しています。
ServiceNowの実装
全体図に示した通り各コンポーネント作成していきます。
なお、使用したServiceNowの環境はVancouverのPDIです。
1.REST API用プロファイル格納用テーブルの作成
REST API接続に使用するパラメーターの内、必須のものと、まあまあ使いそうなものをプロファイルとして保存する拡張テーブルを作成します。
作成した拡張テーブルに情報を管理するための以下フィールドを作成していきます。
※表以外の設定値はスクリーンショットを参照してください。
Column label | Type | Max length |
---|---|---|
Name | String | 40 |
Endpoint | URL | 1024 |
API Key | Password (2 Way Encrypted) | 255 |
Model | String | 40 |
Max tokens | Integer | 5 |
Temperature | Floating Point Number | 40 |
TOP_P | Floating Point Number | 40 |
Presence penalty | Floating Point Number | 40 |
Frequency penalty | Floating Point Number | 40 |
System text | String | 1000 |
フォームレイアウトは自動で追加されたレイアウトから少しだけ修正しました。
2.Script Includeの作成と動作確認
「GenAIconnector」という名前で以下のコードを記載したScript Includeを作成します。
var GenAIconnector = Class.create();
GenAIconnector.prototype = {
AUTO_INIT_PARAMETER: true,
initialize: function(profileID) {
if (!gs.nil(profileID))
this.setProfile(profileID);
},
setProfile: function(profileID) {
if (typeof(profileID) != 'string')
return false;
profileID = profileID.trim();
if (gs.nil(profileID))
return false;
var gr = new GlideRecord('u_gen_ai_connect_profile');
gr.addQuery('u_name', profileID).addOrCondition('sys_id', profileID);
gr.query();
if (gr.next()) {
this.PROFILE_NAME = gr.getValue('u_name');
this.PROFILE_ID = gr.getValue('sys_id');
this.SERVICE = gr.getValue('u_service');
return true;
} else {
return false;
}
},
autoInitParameter: function(bool) {
if (typeof(bool) == 'boolean')
this.AUTO_INIT_PARAMETER = bool;
return this.AUTO_INIT_PARAMETER;
},
initParameter: function() {
var gr = new GlideRecord('u_gen_ai_connect_profile');
if (gr.get(this.PROFILE_ID)) {
// init common parameter
this.MESSAGES = [];
this.RESPONSE = null;
this.HTTP_STATUS = 0;
// init REST API paramter
this.ENDPOINT = gr.getValue('u_endpoint');
this.API_KEY = gr.u_api_key.getDecryptedValue();
this.MODEL = gr.u_model.nil() ? null : gr.getValue('u_model');
this.MAX_TOKENS = gr.u_max_tokens.nil() ? null : Number(gr.getValue('u_max_tokens'));
this.TEMPERATURE = gr.u_temperature.nil() ? null : Number(gr.getValue('u_temperature'));
this.TOP_P = gr.u_top_p.nil() ? null : Number(gr.getValue('u_top_p'));
this.PRESENCE_PENALTY = gr.u_presence_penalty.nil() ? null : Number(gr.getValue('u_presence_penalty'));
this.FREQUENCY_PENALTY = gr.u_frequency_penalty.nil() ? null : Number(gr.getValue('u_frequency_penalty'));
if (!gr.u_system_text.nil())
this.setSystemMessage(gr.getValue('u_system_text'));
return true;
} else {
return false;
}
},
chat: function(prompt, systemText) {
if (typeof(prompt) != 'string')
return;
prompt = prompt.trim();
if (gs.nil(prompt))
return;
if (this.autoInitParameter()) {
if (gs.nil(this.PROFILE_ID))
return;
if (!this.initParameter())
return;
}
this.setSystemMessage(systemText.trim(), true);
this.setUserMessage(prompt);
var request = new sn_ws.RESTMessageV2();
request.setEndpoint(this.ENDPOINT);
request.setHttpMethod('POST');
if (this.SERVICE == 'azure')
request.setRequestHeader('api-key', this.API_KEY);
else
request.setRequestHeader('Authorization', 'Bearer ' + this.API_KEY);
request.setRequestHeader('Content-type', 'application/json');
var body = {};
body.messages = this.MESSAGES;
body.model = (this.SERVICE == 'openai') ? this.MODEL : null;
body.max_tokens = this.MAX_TOKENS;
body.temperature = this.TEMPERATURE;
body.top_p = this.TOP_P;
body.frequency_penalty = this.FREQUENCY_PENALTY;
body.presence_penalty = this.PRESENCE_PENALTY;
for (var key in body) {
if (body[key] === null)
delete body[key];
}
request.setRequestBody(JSON.stringify(body));
var response = request.execute();
this.RESPONSE = JSON.parse(response.getBody());
this.HTTP_STATUS = response.getStatusCode();
return this.RESPONSE.choices[0].message.content.trim();
},
setSystemMessage: function(content, override) {
return this._setMessage('system', content, override);
},
setAssistantMessage: function(content, override) {
return this._setMessage('assistant', content, override);
},
setUserMessage: function(content, override) {
return this._setMessage('user', content, override);
},
_setMessage: function(role, content, override) {
var roleList = ['system', 'assistant', 'user'];
if (gs.nil(role) || gs.nil(content))
return false;
if (roleList.indexOf(role) < 0)
return false;
if (override && typeof(override) == 'boolean') {
for (var i = 0; i < this.MESSAGES.length;) {
if (this.MESSAGES[i].role == role)
this.MESSAGES.splice(i, 1);
else
i++;
}
}
this.MESSAGES.push({
'role': role,
'content': content
});
return true;
},
type: 'GenAIconnector'
};
Script Include作成後はスクリプト単体での動作確認のため、以下の最小のコード量で動作確認をします。
new GenAIconnector("プロファイル名 or sys_id").chat("ユーザープロンプト");
うまく動作したようです。Google検索結果から回答内容自体も正しいようです。
3.Service Catalog(Catalog item)の実装と動作確認
これまで作成したコンポーネントを組み込んだCatalog itemを作成します。
Variableは以下の3つを作成します。
1. プロファイル選択用
2. 選択したプロファイルの説明表示用
3. ユーザー文章入力用
Catalog itemで使用するFlowを作成します。
ChatGPTへ処理を投げる部分は再利用やノーコードでの活用を考慮して新たなActionとして作成しても良かったのですが、 めんどくさい ライセンスの観点4からあえてFlowの中で直接コードを書いています。
動作確認のため、作成したCatalog itemをポータルから申請してみます。
生成されたJavaScriptコードは若干想像していたものと違いましたが内容としては問題なさそうです。
これで用途に応じたプロファイルを作成すればServiceNow上でノーコードに様々な処理を実現できると思います。
作ったもの
需要なさそうですがせっかく作ったのでUpdate setのXMLファイルをGitHubに置いておきます。