見出し画像

【春の自由研究】 kintoneでChatGPTと会話するアプリを作ってみる(アプリの改善&カスタマイズ)

はじめに

みなさんこんにちは。
サイボウズ公認 kintoneエバンジェリスト プロジェクト・アスノートの松田です。

今話題のChatGPTですが、もうみなさん使ってみましたか?
今回は、ChatGPT APIを使って、GPTと会話をするkintoneアプリを作ってみましたので、紹介します。

ステップ1:先人の知恵を借りる

まずは先人の知恵を拝借してみましょう。

kintoneカスタマイズのマニアックなテーマを検索すると、いつも出てくるのが、じゅりどんさんの記事です。
まずは、この記事を参考にしながら、基本的なアプリを作ってみました。

大まかな手順は次のとおり

  1. ChatGPT APIの設定
    未登録の場合は登録を行う。
    私はすでにChatGPT Plusに入っていたので、APIの利用登録・支払い方法の登録を行いました。

  2. API Keyの取得
    APIの管理画面から作成することができます。
    一度作成すると、再度参照することができないので要注意。どこかに保存しておきましょう。

  3. kintoneアプリの用意
    まずは最低限のフィールドを準備し、APIとの会話ができることを確認します。

  4. コーディング
    まずは、じゅりどんさんの記事のコードを拝借してみました。
    ほぼ同じなので、上のリンク先の記事を参照してください。

kintoneアプリ(ステップ1)構成

フィールド一覧:

そして、JavaScriptカスタマイズを入れて、デバッグ後、動作確認を行いました。

動作確認

無事にAIからの回答が表示されました!!
ChatGPTとの通信時間があるので、ちょっと待ち時間が発生してしまうのは仕方ないですね。

課題(改善ネタ)

単純なやり取り(質問&回答)であれば、これで使えそうですね。
kintoneアプリのフィールドにデータを保存することができますので、会話の履歴を保存することができます。
ただし、会話というのは一回で終わることは少なく、何回かのやり取りを行いながら、回答を作り上げていくというのが、ChatGPTのうまい使い方だと思いますので、複数回の履歴を残しておきたくなります

また、GPTからの回答は、Web画面での表示は、構造的な文章で表示されます。箇条書きや番号付きの箇条書き、それから表形式の表示も行われます。画面表示を見ていると、返答はマークダウン記法で返されているようです。
今回のアプリは、アプリの回答フィールドはリッチエディターにしましたが、マークダウンの記載がそのままベタで表示されてしまいました。上記の画像でもわかるように、箇条書きが改行なしでベタ書きされていますね。
これをリッチエディターフィールド内で構造的な文章表示を行えるようにしたいです。

また、ChatGPTとの通信による待ち時間がありますが、今回はレコード編集画面や、新規レコード作成画面で動作させていますので、どうも手持ち無沙汰になりますし、慣れていないと待ちきれずに別の操作をしてしまったりすることも考えられます。待ち時間の間は、スピナー(画面の操作をできなくし、処理中を表すグルグル?を表示する)を表示させたいですね。

あとは、ボタンももう少しカッコよくしておきましょう。
ここはやっぱりkintoneっぽいボタンに変えることにしたいと思います。

ステップ2:アプリを改善する

ステップ1の動作は確認できました。そこで出てきた次の4つの課題を解決するために、改善を進めていきます。

  1. 会話の履歴を残したい

  2. 構造的な文章表示をさせたい

  3. 待ち時間にはスピナーを表示する

  4. ボタンをカッコよくする

方針を考える①履歴

まず、会話の履歴を残したい。これはkintoneに慣れた方ならピンと来ると思いますが、テーブルを利用することにしたいと思います。
アプリの構成を次のように変更しました。

履歴tableを追加しました
フォームの下部にテーブルを設置

このようなアプリレイアウトに変更して、ChatGPTから返ってきた回答を回答フィールドに格納後、下のテーブルに追加していく、という処理を追加すれば良さそうですね。

方針を考える②構造的文章

ChatGPTからの返答はマークダウン形式、kintoneのリッチエディターフィールドではHTML形式。
マークダウン→HTMLへの変換をすればうまくいきそうです。
いろいろやり方はあるようですが、今回は変換用のライブラリを使うことにします。

今回使用するのは、Cybozu CDNにもある、Marked.jsを使います。
下のリンクの村濱さんの記事も参考になります。ググると他にも参考記事が出てきますね!

Marked.jsを使って社内ドキュメントを書きやすくしよう!

方針を考える③スピナー表示

待ち時間のスピナーですが、標準で用意されている、kintone UI Componentを利用することにします。

使用方法や設定の詳細は公式ドキュメントを参考にしました。
ドキュメント:

方針を考える④ボタンをカッコよく

ボタンについても、スピナーと同様、kintone UI Componentを使いました。
これを使うことで、kintone標準のボタンと同様のスタイルのボタンを簡単に設置することができます。

コーディング

コーディングのススメ方

以下に、最終的なJavaScriptコードを掲載します。
実際に作っていく過程では、このような最終的なコードを一気に書くのではなく、一つ一つ機能を作りながら、動かしてみてチェック→デバッグ→調べる→修正→→ を繰り返しながら、少しずつ作っていきます。
今回であれば、以下のようなステップで進めていきました

  1. マークダウンのHTML変換
    リッチエディターへの書式反映の確認

  2. テーブルへの履歴蓄積処理

    1. テーブルへの1行分のデータ追加

    2. 既存テーブル行+追加行による更新

  3. ボタンの変更

  4. スピナーの実装

    1. スピナーON/OFFのタイミングをいろいろ試して調整

  5. コーディング全体の見直し(リファクタリング)

    1. 各種定義の変数化

    2. 変数定義のまとめ

    3. 関数化による全体構造を分かりやすく

ちょっと多機能なものを一気に書いて動かそうとすると、まず一発で動くことはないのですが、それよりも「問題点の切り分け」が難しくなってしまい、デバッグが難しくなってしまいます。
なので、面倒そうに見えますが、1段1段積み上げるようにしながら、
『ここまでは動いた』を積み上げていくことが大事だと思います。
(コレものすごく大事)

使用したライブラリ

talkToGPT.jsは以下のコード
カスタマイズファイルより上にライブラリを設置する

JavaScriptコード

API Keyの部分は、各自のkeyに置き換えてください。

// ChatGPTとの対話アプリ
//  2023/05/07  Shotaro Matsuda
//
(() => {
  "use strict";

  // 各種定義
  const kuc = Kucs["1.10.0"];
  const apiEndpoint = "https://api.openai.com/v1/chat/completions";
  const apiKey = "sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
  const recordKey = "履歴table";
  const promptKey = "プロンプト";
  const answerKey = "回答";
  const promptKeyTbl = "プロンプト_tbl";
  const answerKeyTbl = "回答_tbl";

  // メイン処理
  const chat = async (event) => {
    const sp = kintone.app.record.getSpaceElement("getOpinion");
    const btn = createChatButton();
    sp.appendChild(btn);

    const spinner = createSpinner();

    // ボタンクリック時の処理
    btn.addEventListener("click", async () => {
      spinner.open();
      const record = kintone.app.record.get();

      const userPrompt = record.record[promptKey].value;
      const response = await requestGPTResponse(userPrompt);
      
      // Markdown => HTMLへの変換
      const result = marked.parse(response["choices"][0]["message"]["content"]);

      record.record[answerKey].value = result;
      updateHistoryTable(record, userPrompt, result);

      spinner.close();
      kintone.app.record.set(record);
    });

    return event;
  };

  // ボタン生成 (kintone UI Component)
  const createChatButton = () => {
    return new kuc.Button({
      text: "Chat to GPT",
      type: "normal",
      className: "options-class",
      id: "options-id",
      visible: true,
      disabled: false,
    });
  };

  // Spinner生成 (kintone UI Component)
  const createSpinner = () => {
    return new kuc.Spinner({
      text: "talking to GPT...",
    });
  };

  // GPTとの通信処理 (kintone.proxy)
  const requestGPTResponse = async (prompt) => {
    const resp = await kintone.proxy(apiEndpoint, "POST", {
      "Content-Type": "application/json",
      "Authorization": `Bearer ${apiKey}`,
    }, {
      "model": "gpt-3.5-turbo",
      "messages": [
        {
          "role": "system",
          "content": "日本語で返答してください。",
        },
        {
          "role": "user",
          "content": prompt,
        },
      ],
    });
    return JSON.parse(resp[0]);
  };

  // 履歴テーブルへの更新処理(テーブル最上部へ行追加)
  const updateHistoryTable = (record, prompt, answer) => {
    if (record.record[recordKey].value[0].value[promptKeyTbl].value === undefined) {
      record.record[recordKey].value[0].value[promptKeyTbl].value = prompt;
      record.record[recordKey].value[0].value[answerKeyTbl].value = answer;
    } else {
      const newRow = {
        value: {
          [promptKeyTbl]: {
            type: "MULTI_LINE_TEXT",
            value: prompt,
          },
          [answerKeyTbl]: {
            type: "RICH_TEXT",
            value: answer,
          },
        },
      };
      record.record[recordKey].value.unshift(newRow);
    }
  };

  // kintoneイベントハンドラーの登録
  kintone.events.on(["app.record.create.show", "app.record.edit.show"], chat);

})();


動作確認

今回考えた改善ポイントを盛り込んだアプリが無事動作しました!
回答の待ち時間には、ちゃんとスピナーも表示されましたし、会話の履歴はテーブルに保存されます。

箇条書きが正しく表示された!
会話の履歴はテーブルに蓄積。最新行が上に追加

初心者向けポイント解説

1. kintoneイベント

今回行いたい処理は、編集画面にボタンを設置し、ボタンを押したらChatGPTとの会話を行い、回答を編集中のフィールドに格納するという処理です。

このような処理を作るときには、まずボタンを表示したいkintoneの画面表示イベントを使います。

  • 新規レコード作成画面表示後イベント

  • レコード編集画面表示後イベント

ここで各種処理を動かしていきますが、編集中のフィールドのデータを更新する際には注意が必要です。
ボタンがクリックされたときには、既にイベントハンドラ(chat関数)は終わっている。すなわち関数の最後の " return event; "まで処理されている。
ということです。

cybozu developer networkのドキュメントを読むと、eventオブジェクトを書き換えて、eventオブジェクトをリターンすることでフィールドの値を更新できる、とあります。
通常のイベントハンドラ内の処理でのフィールド値更新はこの方法で行うことができます。
が、今回行いたい処理は、「ボタンを押したときの処理」で、kintoneのイベントハンドラ内の処理ではありません(先ほど言った、もう終わっているというやつ)。
このような場合には、event.recordオブジェクトの書き換えは使えません。
代わりに、kintone.app.record.get() → kintone.app.record.set()を使用します。
kintone.app.record.get()で取ってきた、フォームのデータを書き換えて、それを、kintone.app.record.set()で反映してあげることができます。

// この時点の編集中レコードのデータを取得
const record = kintone.app.record.get();

// ChatGPTと会話し、回答を得る
const userPrompt = record.record[promptKey].value;
const response = await requestGPTResponse(userPrompt);

// Markdown => HTMLへの変換
const result = marked.parse(response["choices"][0]["message"]["content"]);

//取得したレコードオブジェクトの書き換え
record.record[answerKey].value = result;
updateHistoryTable(record, userPrompt, result);

// 書き換えたレコードデータをフォームに反映
kintone.app.record.set(record);

2. テーブルへのデータ追加

// 履歴テーブルへの更新処理(テーブル最上部へ行追加)
const updateHistoryTable = (record, prompt, answer) => {
    // テーブルに行データがない場合(新規レコードの一発目)の処理
    // 既存行にデータを追加する (初期表示の行)
    if (record.record[recordKey].value[0].value[promptKeyTbl].value === undefined) {
        record.record[recordKey].value[0].value[promptKeyTbl].value = prompt;
        record.record[recordKey].value[0].value[answerKeyTbl].value = answer;
    } else {
        // 既存行がある場合の処理: 追加する行データを作成
        const newRow = {
            value: {
                [promptKeyTbl]: {
                    type: "MULTI_LINE_TEXT",
                    value: prompt,
                },
                [answerKeyTbl]: {
                    type: "RICH_TEXT",
                    value: answer,
                },
            },
        };
        // 既存行データの配列の先頭に、追加する行データを追加:unshift
        record.record[recordKey].value.unshift(newRow);
    }
};

新規レコード作成画面を開くとわかるように、テーブルには初期値として1行が表示されています。
なので、新規レコードの一発目のデータを入れる際は、行追加ではなく、既存の1行目にデータを入れてあげる必要があります。

2行目以降については、追加する行データ(オブジェクト)を生成し、unshiftを用いて、既存行の配列の先頭に追加してあげました。

さらなる改善ポイント?

このアプリを使っていて、重要なことに気が付きました。
WebでChatGPTを利用する場合、履歴と学習の記録をオンにしていると、1つのチャット内で会話した内容を覚えてくれていて、「さっきのあれ」という聞き方ができるんですよね。
ところが、APIによる会話では、この学習機能が強制的にオフになっていて、会話の履歴を使った会話が通常ではできないようです。

そこでこの仕様について、ChatGPTに聞いてみました。

自らの仕様についても、詳しく教えてくれました。
毎回の会話に、これまでの会話履歴をくっつけて送信することで、履歴を反映した会話ができるようです。
履歴を生かした会話を行うためには、このあたりを改善してみるとよさそうです。

さいごに

今回は、ChatGPT APIを使って、kintoneアプリからチャットを行い、その履歴をアプリに記録するというカスタマイズ例を紹介しました。

と同時に、kintoneカスタマイズを考えたり、改善していくときの考え方についても解説しました。kintoneのカスタマイズを勉強中であったり、これから学ぼうと思っている方の参考になれば嬉しいです。

さて、毎週日曜日の夜22時から、YouTube Liveをやっています。
「今夜もkintone」という番組名で、最新kintoneニュースやコミュニティイベント情報や、kintoneのアップデート情報、それからkintone活用や業務改善のノウハウについて発信していますので、ぜひチャンネル登録よろしくお願いします!

kintoneのいろんなシーンでChatGPTを使ってみた回


いただいたサポートは、今後とも有益な情報を提供する活動資金として活用させていただきます! 対価というよりも、応援のキモチでいただけたら嬉しいです。