Skip to content
10月 28, 2024
14 min read time

生成AI活用 kintoneでワンクリック議事録生成

議事録のテキストを分割して登録

今回はボタンを押して項目ごとにわけて、議事録が出来るよというものです

 

イメージ動画はこちら

 

目次

・はじめに

・実際のコード

・このコードの改変について

・どんな利用方法が考えられるのか

・どんな流れで作成したのか

 

はじめに

今どき、オンライン会議ツールでは気軽に音声起こしできますよね。

ものによっては要約とかもできるっぽいですが...

 

それはさておき、気軽にkintoneで管理できるといいなとも思うわけですね

 

これは社内環境ありきなのですが、Teamsを使っていまして。

レコーディングするとですね。ある一定期間で削除しましたよのメールがたまに届くんですね。

 

スクリーンショット 2024-10-26 143059

 

 

であれば、ちゃんと記録しておきたいなと思い、kintone上でなにかできないかな。生成AI絡めて議事録として使えないかなとなりました。

 

やりたいこと

文字起しを入力 ⇒ 議事録にする (要約だけでなく項目分けもする)

 

おそらく理想形は、ミーティングツールからの直の連携だったり、映像や音声記録からやることでしょう。

ただし、ファイルの取り扱いが絡むところでやり方が分かり切らず、一旦本記事の中ではテキスト情報としてやりました。

 

多少無駄のあるコードも含むかもしれませんが、ご容赦下さい。

※公式ドキュメントなども参照してください

※コードの導入についても公式のドキュメントに分かりやすくまとまっています

 

実際のコード


(() => {
  'use strict';

  // ChatGPT APIのエンドポイントURL
  const apiUrl = 'https://api.openai.com/v1/chat/completions';
  // ChatGPT APIキーの指定
  const apiKey = 'OpenAIのAPIキー';

  // ChatGPT APIにリクエストを送信する関数
  const sendChatGPTRequest = async (originalText) => {
    const prompt = `
      以下の文章を要約し、次の項目に分類してください。各項目の内容は、項目名の後に ': ' を付けて記述してください:

      議題:
      主要な論点:
      詳細:
      次のアクションプラン等:

      原文:
      ${originalText}
    `;

    const response = await fetch(apiUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${apiKey}`
      },
      body: JSON.stringify({
        model: 'gpt-4o-mini',  // または 'gpt-4' など、適切なモデルを指定
        messages: [
          { role: 'system', content: 'You are a helpful assistant that summarizes text and extracts key information.' },
          { role: 'user', content: prompt }
        ]
      })
    });

    const parsedResponse = await response.json();

    if (!response.ok) {
      const error = parsedResponse.error;
      throw new Error(`${error.code}: ${error.message}`);
    }

    return parsedResponse.choices[0].message.content;
  };

  // 応答を項目ごとに分類する関数
  const categorizeResponse = (response) => {
    const categories = ['議題', '主要な論点', '詳細', '次のアクションプラン等'];
    const result = {};

    categories.forEach(category => {
      const regex = new RegExp(`${category}:\\s*(.+?)(?=\\n\\n|$)`, 's');
      const match = response.match(regex);
      result[category] = match ? match[1].trim() : '';
    });

    return result;
  };

  // 文章を要約し、各項目に分類する関数
  const summarizeAndClassify = async (record) => {
    const originalText = record['原文'].value;
    if (!originalText) {
      throw new Error('原文が入力されていません。');
    }

    const response = await sendChatGPTRequest(originalText);
    return categorizeResponse(response);
  };

  kintone.events.on('app.record.detail.show', (event) => {
    const record = event.record;

    const menuButton = document.createElement('button');
    menuButton.id = 'summarize_button';
    menuButton.innerText = '要約と分類';
    menuButton.onclick = async function() {
      try {
        menuButton.disabled = true;
        menuButton.innerText = '処理中...';

        const appId = kintone.app.getId();
        const recordId = kintone.app.record.getId();

        // レコードを取得
        const body = {
          app: appId,
          id: recordId
        };
        const res = await kintone.api(kintone.api.url('/k/v1/record.json', true), 'GET', body);
        const currentRecord = res.record;

        const result = await summarizeAndClassify(currentRecord);

        // 更新のパラメータを宣言
        const putBody = {
          app: appId,
          id: recordId,
          record: {
            '議題': { value: result['議題'] },
            '主要な論点': { value: result['主要な論点'] },
            '詳細': { value: result['詳細'] },
            '次のアクションプラン等': { value: result['次のアクションプラン等'] }
          }
        };

        // 非同期で更新
        await kintone.api(kintone.api.url('/k/v1/record.json', true), 'PUT', putBody);

        alert('要約と分類が完了しました。');

        // 画面の更新
        location.reload();
      } catch (error) {
        console.error('エラー:', error);
        alert(`エラーが発生しました: ${error.message}`);
      } finally {
        menuButton.disabled = false;
        menuButton.innerText = '要約と分類';
      }
    };

    const headerMenuSpace = kintone.app.record.getHeaderMenuSpaceElement();
    headerMenuSpace.appendChild(menuButton);

    return event;
  });
})();

このコードの改変について

基本機能をこのままにする前提ですが、2点改変ポイントがあります。

・システムプロンプト

{ role: 'system', content: 'You are a helpful assistant that summarizes text and extracts key information.' }

Youからはじまるこの文章です。

ここは役割を持たせる指示文となりますが、実は書かないぐらいの内容でもおそらく成り立ちます。

特に議事録の要約ということであれば、特別な役割もなくChatGPTであればこなしてしまうでしょう。ただし改変という事を意識するにあたって、どのような目線でとか。大前提ここは外してほしくないというものがあれば書いておくとよいでしょう。

 

・項目の整理

const prompt = `
      以下の文章を要約し、次の項目に分類してください。各項目の内容は、項目名の後に ': ' を付けて記述してください:

 

      議題:
      主要な論点:
      詳細:
      次のアクションプラン等:

 

      原文:
      ${originalText}
 

一部抜粋してみました。現在の流れでは、"原文"としている文章を基に4つの項目にまとめるようになっています。

この4項目は必要に応じて増減できるところなので、議事録まとめるときのフォーマットがあればその内容にそって項目建てを行うとよいでしょう。

 

あとは原文を基に要約を行っています。これは同様に、違う項目をkintone上に立てて、その項目を参照してChatGPTに内容を伝えることもしめしています。

例えば、議事録に含まれていない前後関係のメモを参照したり、またはその時に湧いてきたアイデアを参照するとか。拡張性はあると思います。

 

 

 

どんな利用方法が考えられるのか

議事録として仮に作っていますが。例えばヒアリング文章だったり。

なにか長文を要する業務全般において活用できるのではないかと思います。

また改変の話と一緒に考えると、誰が何を聞いて何の目的のために整理するのか。

 

例えば社内の1on1ミーティングであれば、持っている悩みや課題感、要望とかもあるかもしれません。

 

 

どんな流れで作成したのか

全体を通して生成AIと相談しながら進行していました

 

①動作確認まで

なぜか入力が終わりましたと出るけど、内容が入力されない...

エラーが出なかったのがやっかいでした。一応エラーが出るようにはしているので、エラー文が出ればそこから推測が出来ることもある。

なので今回は順番を意識して1つずつ確認することにしました。

原文の入力 ⇒ gpt-4o-mini へ要約の依頼 ⇒ 返答を受け取る ⇒ kintoneに入力

 

原文の入力はkintone上で文字を打ち込むだけのことなので、大前提として解決できます。

 

スクリーンショット 2024-10-26 143618_002

 

もう一つ、kintone上での取り扱いとして各フィールドの名称も、コードに合うようにしっかり設定しておきましょう。

---

スクリーンショット 2024-10-26 144358

---

 

②gpt-4o-mini へ要約の依頼

じゃあ、そもそも原文をgpt-40-miniに送ることが出来ているのでしょうか。

ここは、OpenAI側で確認することが出来ます。

 

スクリーンショット 2024-10-26 145501

 

リアルタイムに依頼の量・料金が出るようになっています。ここが出ていれば依頼は送ることが出来ています。

 

③返答を受け取る

依頼は送ることが出来ていました。では次に返答を受け取ることが出来ているかを知りたい。

kintone上でダイアログでとしてまずはシンプルに出せるようにしてみましょう。


  // ダイアログを作成する関数
  const createResponseDialog = (result) => {
    const dialog = document.createElement('div');
    dialog.id = 'response-dialog';
    dialog.style.cssText = `
      position: fixed;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      background: white;
      padding: 20px;
      border-radius: 5px;
      box-shadow: 0 0 10px rgba(0,0,0,0.3);
      z-index: 1000;
      max-width: 80%;
      max-height: 80%;
      overflow-y: auto;
    `;

    const content = document.createElement('div');
    for (const [key, value] of Object.entries(result)) {
      const item = document.createElement('p');
      item.innerHTML = `<strong>${key}:</strong> ${value}`;
      content.appendChild(item);
    }
    dialog.appendChild(content);

    const closeButton = document.createElement('button');
    closeButton.textContent = '閉じる';
    closeButton.onclick = () => dialog.remove();
    dialog.appendChild(closeButton);

    return dialog;
  };

  // 文章を要約し、各項目に分類する関数
  const summarizeAndClassify = async (record) => {
    const originalText = record['原文'].value;
    if (!originalText) {
      throw new Error('原文が入力されていません。');
    }

    const response = await sendChatGPTRequest(originalText);
    return categorizeResponse(response);
  };

  kintone.events.on('app.record.detail.show', (event) => {
    const record = event.record;

    const menuButton = document.createElement('button');
    menuButton.id = 'summarize_button';
    menuButton.innerText = '要約と分類';
    menuButton.onclick = async function() {
      try {
        menuButton.disabled = true;
        menuButton.innerText = '処理中...';

        const result = await summarizeAndClassify(record);

        // 結果を各フィールドに設定
        record['議題'].value = result['議題'];
        record['主要な論点'].value = result['主要な論点'];
        record['詳細'].value = result['詳細'];
        record['次のアクションプラン等'].value = result['次のアクションプラン等'];

        // レコードを更新
        kintone.app.record.set({ record: record });
        
        // UIを強制的に更新
        const updatedRecord = kintone.app.record.get();
        kintone.app.record.set({ record: updatedRecord });

        // 結果をダイアログで表示
        const dialog = createResponseDialog(result);
        document.body.appendChild(dialog);

        alert('要約と分類が完了しました。');
      } catch (error) {
        console.error('エラー:', error);
        alert(`エラーが発生しました: ${error.message}`);
      } finally {
        menuButton.disabled = false;
        menuButton.innerText = '要約と分類';
      }
    };

    const headerMenuSpace = kintone.app.record.getHeaderMenuSpaceElement();
    headerMenuSpace.appendChild(menuButton);

    return event;
  });
})();

 

上記コードを一部含むことで、枠には入力されないけど、動作完了時に生成AIからの返答情報だけ表示されるようになります。

ここまでも無事に出来ていることが確認できました。

AIに文章を送ってその返事が返ってきているというのは安心ですね。

 

④kintoneに入力

④の段階で私は詰まっていたようでした。

なぜか最初は枠の中にgpt-4o-miniかな返事が返ってこない。

 

ちなみに適切でなかったコードの残りが③の中に含まれています(笑)

どうも更新の処理をちゃんと出来ていなかった様子。

 

ということで「kintone 入力 etc 」とかググって参考になりそうな記事を探していました。

公式のドキュメントも参考にしながら無事入力ができました!

 

 

 

スクリーンショット 2024-10-26 143618_003

 

 

気軽に生成AIをビジネスツールに含めることは便利ですね。

4DLでは(千葉市に限りません!)こういった活用のお手伝いも行っています。

お気軽にご相談いただければと思います。

banner

 

また定期的にワークショップも開催しています。

https://4dlt.com/makuhari_prompt_study

こちらも覗きに来ていただけたら嬉しいです!

 

Create your account today!

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique.