6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ServiceNowとChatGPTをREST APIで繋げてみる

Posted at

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):エンドユーザーが利用する

image.png

ChatGPTの準備

まずはともあれChatGPT側の環境が準備されていないことには何もできないのでREST API接続できる環境を準備します。
(多分他の記事でたくさん書かれているので)詳細は省きますが、個人でまず試してみたいという方は準備のハードルが低い3OpenAIのサービスを利用されるのが良いかと思います。
※この記事ではAzure OpenAIサービスを使用しています。

ServiceNowの実装

全体図に示した通り各コンポーネント作成していきます。
なお、使用したServiceNowの環境はVancouverのPDIです。

1.REST API用プロファイル格納用テーブルの作成

REST API接続に使用するパラメーターの内、必須のものと、まあまあ使いそうなものをプロファイルとして保存する拡張テーブルを作成します。

[Mainセクション]
image.png

[Controlsセクション]
image.png

[Application Accessセクション]
image.png

作成した拡張テーブルに情報を管理するための以下フィールドを作成していきます。
※表以外の設定値はスクリーンショットを参照してください。

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

[Dictionary一覧]
image.png

フォームレイアウトは自動で追加されたレイアウトから少しだけ修正しました。

[拡張テーブルのフォーム]
image.png

2.Script Includeの作成と動作確認

「GenAIconnector」という名前で以下のコードを記載したScript Includeを作成します。

GenAIconnector
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("ユーザープロンプト");

[実行結果]
Screenshot 2024-03-19 22.12.19.png

うまく動作したようです。Google検索結果から回答内容自体も正しいようです。

image.png

3.Service Catalog(Catalog item)の実装と動作確認

これまで作成したコンポーネントを組み込んだCatalog itemを作成します。

[Catalog item]
image.png

Variableは以下の3つを作成します。

1. プロファイル選択用
2. 選択したプロファイルの説明表示用
3. ユーザー文章入力用

[Variables]
image.png

Catalog itemで使用するFlowを作成します。
ChatGPTへ処理を投げる部分は再利用やノーコードでの活用を考慮して新たなActionとして作成しても良かったのですが、 めんどくさい ライセンスの観点4からあえてFlowの中で直接コードを書いています。

[Flow designer]
image.png

動作確認のため、作成したCatalog itemをポータルから申請してみます。

[Catalog item申請画面]
image.png

生成されたJavaScriptコードは若干想像していたものと違いましたが内容としては問題なさそうです。

[Request item処理結果]
image.png

[コンソールでの実行結果]
image.png

これで用途に応じたプロファイルを作成すればServiceNow上でノーコードに様々な処理を実現できると思います。

作ったもの

需要なさそうですがせっかく作ったのでUpdate setのXMLファイルをGitHubに置いておきます。

  1. インシデントの要約、解決メモの生成、コード生成、Flow自動生成など

  2. ITSMの場合、Pro以上かつ追加SKUで1Fulfillerあたりざっくり2倍ぐらい

  3. Azure OpenAIのような申請も不要で3ヶ月間有効な無料クレジットあり

  4. Action内のScript stepで実行される外部へのREST API接続はIntegration Hubのトランザクションとして計上されるっぽい

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?