Amazonで商品を見るセール会場へ

GAS│Google Apps Scriptで作る!ChatGPT風チャットボットの作り方入門

GAS(Google Apps Script)とOpenAI APIを使い、質問と回答をスプレッドシートに保存する個人用のチャット画面を作ります。

ChatGPTのWeb版そのものを組み込む仕組みではなく、OpenAI APIのテキスト応答をGASのWebアプリに表示する仕組みです。

これにより、ブラウザ上で動作する簡易的なChatGPTチャットボットを作成できます。

ブラウザ上で動作するチャットボットの完成イメージ。

本記事のコードは自分だけがアクセスできる設定で試します。利用者別の認証・履歴分離・利用量制限は備えていません。他の人に公開する場合は、それらを追加してから公開範囲を広げてください。

質問と回答はGoogleスプレッドシートに記録されます。ただし、APIに送るのは毎回の質問だけです。履歴の再表示はできますが、過去の文脈を引き継ぐ対話にはなりません。

OpenAI APIの料金はChatGPTの契約とは別です。入力はOpenAIへ送信され、ログは指定のシートに残ります。個人情報や機密情報を入れず、APIの利用額とGASの割り当てを確認しながら試してください。

目次

GASで動くChatGPTの作り方

UIと処理ロジックはGASで構築し、ログ保存にはGoogleスプレッドシートを使用します。

また、ChatGPTを利用するためにOpenAIのAPIキーを発行します。

これらの連携により、簡易的なチャットボット環境を構築できます。

ChatGPTのAPIキーの発行

まずはOpenAIの管理画面からChatGPTのAPIキーを発行します。

管理画面で発行されたAPIキーの確認画面。

発行されたシークレットキーは再表示できない場合があるため、必ず控えておきます。

スクリプトプロパティに登録

控えたAPIのシークレットキーを、GASの「スクリプトプロパティ」に登録します。

APIキーはサーバー側のスクリプトプロパティに保存し、HTMLやブラウザに返しません。プロジェクトの編集者はコード経由で参照できるため、GASプロジェクトの編集権限も限定してください。

スクリプトプロパティの編集画面を開いた様子。

今回はプロパティ名を「ACCESS_TOKEN」として設定しました。

「値」の入力欄に先ほどのシークレットキーを設定して保存します。

スクリプトプロパティにプロパティ名とシークレットキーの値を設定して保存する画面。

ログ用のスプレッドシートを作成

ログ専用のGoogleスプレッドシートを新規作成し、1行目のA〜D列に「日時」「会話ID」「質問」「回答」の見出しを入力します。コードは2行目以降を履歴として読みます。シートの閲覧・編集権限は自分だけに限定してください。

作成後、スプレッドシートのID(URL内の該当部分)とシート名を控えておきます。

※控えたIDとシート名は、後述するmain.gs内の設定箇所に記述します。

対話履歴を記録するために新規作成したスプレッドシートの画面。

Webアプリの作成

GASエディタ上に必要な各ファイルを構成し、コードを記述していきます。

エディタ上に構成したWebアプリ用ファイルの一覧。
ファイル備考
app.gsHTMLファイルを読み込み、Webアプリケーションとして表示可能な形式に変換します。
index.html画面のUI(HTML構造)を記述します。
style.html画面デザインを定義するCSSを記述します。
script.html画面の挙動や非同期通信を制御するフロントエンドJavaScriptを記述します。
main.gsChatGPT APIの呼び出しやスプレッドシート連携を行うサーバー側処理を記述します。

app.gs

Webアプリとしてアクセスされた際に、HTMLを出力するための初期化処理を記述するファイルです。

function doGet() {
  return HtmlService.createTemplateFromFile("index").evaluate();
}

index.html

チャット画面の基本構造を定義するファイルです。

画面左側に履歴などを表示するサイドバーを配置し、中央に対話メッセージ領域を設けています。

また、外部ファイル化したCSSとJavaScriptを読み込む記述を含めています。

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <?!= HtmlService.createHtmlOutputFromFile("style").getContent(); ?>
  </head>
  <body>
    <div class="sidebar">
      <h2 onclick="startNewConversation()">新規会話</h2>
      <div id="historyList"></div>
    </div>

    <div class="main">
      <div class="header">
        <label for="modelSelect">モデル選択:</label>
        <select id="modelSelect">
          <option value="gpt-4.1-mini" selected>gpt-4.1-mini</option>
          </select>
      </div>
      <div class="chat-box" id="chatLog"></div>
      <div class="input-area">
        <textarea id="inputText" rows="5" placeholder="メッセージを入力..."></textarea>
        <button onclick="sendMessage()">送信</button>
      </div>
    </div>
    <?!= HtmlService.createHtmlOutputFromFile("script").getContent(); ?>
  </body>
</html>

掲載コードは、テキスト応答用のgpt-4.1-miniに限定した例です。

2026年10月28日時点で、OpenAIはこのモデルのAPI提供を2027年1月14日に終了すると告知しています。

開始前に公式の提供終了予定と、自分のAPIプロジェクトでの利用可否を確認してください。

モデルを変更する場合は、index.htmlの選択肢とmain.gsの許可モデル名を両方変更します。

変更先がChat Completionsと掲載コードのパラメーターに対応するか確認し、短い質問で応答とログ保存を試してください。

後継モデルでの実API通信は未確認で、モデル名だけの置換による動作は保証していません。

音声合成・Realtime用モデルは、このテキスト処理には追加しません。

対応する処理とモデル名はOpenAIのモデル資料、Webアプリの実行者・公開範囲はGASのWebアプリ資料で確認できます。

style.html

チャットUI全体のレイアウトや配色を調整するCSSファイルです。

<style>
  body {
    font-family: sans-serif;
    background-color: #f5f5f5;
    margin: 0;
    padding: 0;
    display: flex;
  }
  .sidebar {
    width: 250px;
    background-color: #e6f0fa;
    border-right: 1px solid #ccc;
    padding: 1em;
    height: 100vh;
    overflow-y: auto;
  }
  .sidebar h2 {
    font-size: 1em;
    margin-top: 0;
    cursor: pointer;
    color: #1a73e8;
  }
  .sidebar .history-item {
    margin: 0.5em 0;
    color: #1a73e8;
    cursor: pointer;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;    
  }
  .main {
    flex-grow: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 1em;
  }
  .header {
    font-size: 1.2em;
    font-weight: bold;
    margin-bottom: 1em;
  }
  .chat-box {
    width: 100%;
    max-width: 800px;
    background-color: #fff;
    padding: 1em;
    box-shadow: 0 2px 5px rgba(0,0,0,0.1);
    min-height: 300px;
    margin-bottom: 1em;
    overflow-y: auto;
  }
  .input-area {
    display: flex;
    width: 100%;
    max-width: 800px;
  }
  textarea#inputText {
    flex: 1;
    padding: 0.5em;
    font-size: 1em;
    resize: vertical;
  }
  button {
    padding: 0.5em 1em;
    margin-left: 0.5em;
    font-size: 1em;
  }
  .message {
    margin: 0.5em 0;
    padding: 0.5em;
  }
  .user {
    color: #1a73e8;
  }
  .bot {
    color: #0b8043;
    white-space: pre-wrap;
  }
</style>

続いて、通信処理を担うmain.gsとフロントエンド処理のscript.htmlを実装します。

すべてのコードを実装したあと、Webアプリとしてデプロイすることでブラウザからアクセス可能になります。

通信処理やフロントエンド処理の実装コードとデプロイ前の状態。

ここから先の詳細な実装コードは有料コンテンツとなります。

Googleスプレッドシートの他の自動化操作については、まとめ記事をご確認ください。

script.html

フロントエンド側のJavaScriptを記述するファイルです。

送信ボタンの押下時にGAS側を呼び出す処理や、サイドバーへの対話履歴の描画を担当します。表示する履歴は最新200行です。入力と応答はHTMLとして実行せず文字列として表示します。

<script>
  let sending = false;
  let conversationId = Date.now().toString();

  const chatLog = document.getElementById("chatLog");
  const input = document.getElementById("inputText");
  const historyList = document.getElementById("historyList");

  function startNewConversation() {
    if (sending) return;
    conversationId = Date.now().toString();
    chatLog.innerHTML = "";
    input.value = "";
    loadSidebar();
  }

  function appendMessage(role, content) {
    const div = document.createElement("div");
    div.className = `message ${role}`;
    div.textContent = `${role === 'user' ? 'あなた' : 'AI'}: ${content}`;
    chatLog.appendChild(div);
  }

  function groupByThread(data) {
    const grouped = {};
    data.forEach(item => {
      if (!grouped[item.thread]) grouped[item.thread] = [];
      grouped[item.thread].push(item);
    });
    return grouped;
  }

  function loadSidebar() {
    google.script.run
      .withSuccessHandler(function(data) {
        historyList.innerHTML = "";
        if (!Array.isArray(data) || data.length === 0) {
          historyList.textContent = "履歴がありません";
          return;
        }

        const grouped = groupByThread(data);
        Object.entries(grouped).reverse().forEach(([threadId, messages]) => {
          const div = document.createElement("div");
          div.className = "history-item";
          div.textContent = messages[0].user;
          div.onclick = () => loadHistoryFromThread(messages);
          historyList.appendChild(div);
        });
      })
      .withFailureHandler(err => {
        console.error("履歴取得に失敗:", err);
        alert("履歴の読み込みに失敗しました。");
      })
      .getHistoryFromSheetWithThreads();
  }

  function loadHistoryFromThread(messages) {
    if (sending) return;
    conversationId = messages[0].thread;
    chatLog.innerHTML = "";

    messages.forEach(msg => {

      appendMessage('user', msg.user);

      appendMessage('bot', msg.bot);
    });
  }

  function sendMessage() {
    const userMsg = input.value.trim();
    if (!userMsg || sending) return;
    if (userMsg.length > 4000) { alert("質問は4000文字以内にしてください。"); return; }
    sending = true;

    appendMessage('user', userMsg);
    input.value = "";


    const modelSelect = document.getElementById("modelSelect");
    const selectedModel = modelSelect.value;

    google.script.run
      .withSuccessHandler(function(response) {
        sending = false;
        appendMessage('bot', response);

        loadSidebar();
      })
      .withFailureHandler(() => {
        sending = false;
        appendMessage('bot', '送信に失敗しました。GASの設定・API利用状況・シートを確認してください。');
      })
      .askChatGPTWithModel(userMsg, conversationId, selectedModel);
  }

  window.onload = loadSidebar;
</script>

main.gs

ChatGPTのAPIへのリクエスト送信やスプレッドシートへの記録処理を記述するファイルです。

冒頭の設定変数に、事前準備で控えたスプレッドシートのIDとシート名を記述します。

ChatGPTのAPIを呼び出すための処理を記述します。

const SHEET_ID = "xxxxxxxxxxxxxxxxxxxxxxxxxxx";
const SHEET_NAME = "シート1";

// デプロイのアクセス権は本人のみに限定する。
function askChatGPTWithModel(userInput, threadId, modelName) {
  if (typeof userInput !== 'string' || !userInput.trim() || userInput.length > 4000) {
    throw new Error('質問は1〜4000文字にしてください。');
  }
  if (typeof threadId !== 'string' || !/^\d{10,20}$/.test(threadId)) {
    throw new Error('会話IDが不正です。');
  }
  if (modelName !== 'gpt-4.1-mini') throw new Error('許可されていないモデルです。');
  const apiKey = PropertiesService.getScriptProperties().getProperty('ACCESS_TOKEN');
  if (!apiKey) throw new Error('APIキーが未設定です。');
  const sheet = getLogSheet_();
  const payload = {
    model: modelName,
    messages: [
      {role: 'system', content: '質問に日本語で簡潔に回答してください。'},
      {role: 'user', content: userInput}
    ],
    max_completion_tokens: 1024
  };
  const response = UrlFetchApp.fetch('https://api.openai.com/v1/chat/completions', {
    method: 'post', contentType: 'application/json',
    headers: {Authorization: 'Bearer ' + apiKey},
    payload: JSON.stringify(payload), muteHttpExceptions: true
  });
  const status = response.getResponseCode();
  if (status < 200 || status >= 300) throw new Error('API通信に失敗しました(HTTP ' + status + ')。');
  const result = JSON.parse(response.getContentText());
  const message = result.choices && result.choices[0] && result.choices[0].message;
  if (!message || typeof message.content !== 'string' || !message.content.trim()) {
    throw new Error('テキスト応答を取得できませんでした。');
  }
  const reply = message.content.trim();
  // =で始まる入力をシートの数式として評価させない。
  const asText = value => /^[=+@-]/.test(value) ? "'" + value : value;
  sheet.appendRow([new Date(), threadId, asText(userInput), asText(reply)]);
  return reply;
}

// 末尾_の関数はgoogle.script.runから直接呼び出せない。
function getLogSheet_() {
  const sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName(SHEET_NAME);
  if (!sheet) throw new Error('ログ用シートが見つかりません。');
  const headers = sheet.getRange(1, 1, 1, 4).getDisplayValues()[0];
  if (headers.join('|') !== '日時|会話ID|質問|回答') {
    throw new Error('A1:D1の見出しを確認してください。');
  }
  return sheet;
}

function getHistoryFromSheetWithThreads() {
  const sheet = getLogSheet_();
  const lastRow = sheet.getLastRow();
  if (lastRow < 2) return [];
  // 表示用文字列として返し、Dateをブラウザに直接渡さない。
  const firstRow = Math.max(2, lastRow - 199);
  return sheet.getRange(firstRow, 1, lastRow - firstRow + 1, 4)
    .getDisplayValues().map(r => ({
      timestamp: r[0], thread: r[1], user: r[2], bot: r[3]
    }));
}

デプロイして表示

GASエディタの「デプロイ」→「新しいデプロイ」で種類をWebアプリにし、実行するユーザーを自分、アクセスできるユーザーを自分のみとする設定でデプロイします。本人限定にできない環境では、この共有ログの例を他人に公開しないでください。コード変更後はデプロイのバージョンも更新します。

エディタ右上のデプロイメニューから新しいデプロイを実行する画面。

デプロイ完了後に発行されるウェブアプリのURLをクリックして開きます。

デプロイ完了後に発行されたウェブアプリのURL画面。

短い質問で応答とシート2行目への保存を確認し、再読み込み後の履歴表示、通信エラー、送信中の再クリックも試します。ログ保存に失敗してもAPI呼び出しの料金は発生し得るため、エラー時に自動再送はしません。掲載コードはローカルの模擬環境で確認した例であり、GAS上でのデプロイ・実API通信・画面表示は利用環境で確認してください。

完成したチャットボットの画面でメッセージを送信し、応答をテストしている様子。

以上で、GASを使ったChatGPT風チャットボットの構築手順は完了です。

関連コードは、GitHubのMOTOKI-LLC/cg-method-codeにもあります。リポジトリと本記事の更新時期は異なるため、この手順では記事内の5ファイルを一式で使用してください。

外部のサービスと連携する記事は、ほかにもあります。

スポンサーリンク

GASチャットボットについてよくある質問

このチャットボットは、何でできていますか?

UIと処理ロジックはGAS(Google Apps Script)で作り、ログの保存にはGoogleスプレッドシートを使います。ブラウザ上で動く簡易的なChatGPT風チャットボットで、対話ログはスプレッドシートに自動で記録されます。

使う人にも、ChatGPTのアカウントが必要ですか?

この例は作成者本人だけがアクセスする設定です。OpenAI APIのキーと利用設定が必要ですが、ChatGPTの有料契約は必要ありません。他人に使ってもらうには、認証・履歴分離・利用量制限を追加する必要があります。

費用は、かかりますか?

ChatGPTのAPIは従量課金制です。利用頻度やトークンの消費量に応じて費用が増えるため、使いすぎに注意してください。料金やモデルの種類は、OpenAIの公式ページで最新を確認してください。

APIキーは、どこに保存しますか?

GASのスクリプトプロパティに登録します。この記事では、プロパティ名をACCESS_TOKENとして設定しました。APIキーをコード内に直接書くと流出の危険があるため、コードには書かないでください。

発行したAPIキーを、なくしてしまいました。

発行されたシークレットキーは再表示できない場合があるため、発行時に必ず控えておきます。控えていない場合は、新しいキーを発行し直してください。

どのファイルを、作成しますか?

GASエディタに、5つのファイルを作成します。app.gs(Webアプリとして表示する処理)、index.html(画面の構造)、style.html(CSS)、script.html(送信ボタンや履歴の描画)、main.gs(APIの呼び出しとスプレッドシート連携)です。

スプレッドシートのIDとシート名は、どこに書きますか?

ログ用のスプレッドシートを新規作成し、URL内のIDとシート名を控えます。それを、main.gsの冒頭にある設定変数に記述します。

使うモデルは、変更できますか?

変更できますが、掲載モデルには提供終了予定があります。

変更前に、index.htmlの直後にある期限と確認手順を確認してください。

次に学ぶ・作業環境を選ぶ

学習を続けたい方や、作業環境を整えたい方は、目的に合うガイドをご覧ください。

スプレッドシート・GASのおすすめ書籍

作業環境の作り方

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次