Code

GASとChatGPT_APIで、マンダラート的目標設定シートを作成してみた!

ChatGPTを使い始めて久しいですが、一番有効に使えるのは、
「コーディングの補助」と、「思考を深めるための壁打ち」だろうと思い始めています。
最近、職場の研修で大谷翔平も実践したという目標設定ツール、オープンウィンドウ64なるものに出会いました。
ツールの詳細は検索したらたくさん出てくるため割愛しますが、思考を掘り下げて自分の目標をクリアにするツールとして面白いものだなと思います。
そして、いつもの癖で「是非これを自動化してみたい」という欲が湧いてきたため挑んでみることにしました。
成果物はこんな感じです。

mandara_demo

プログラム概要

使用言語:GAS(Google Apps Script)
やりたかったこと:目標を書いてボタンを押すと、目標に対する具体的な実践思考をジャンル別に64個自動作成する曼荼羅ツールの作成。

要件定義

  • Google spreadsheet上で作成
  • ChatGPT-APIを利用して文章を自動生成させる
  • 1段階目の操作で、目標に対する8つの基礎思考を作成
  • 2段階目の操作で、8つの基礎思考からそれぞれについて8つの実践思考を作成
  • 表示形式は、オープンウィンドウ64の基本である9×9のマスとする。

  • Spreadsheet上でGASを書く

    まずは適当な名前でスプレッドシートを作成し、拡張機能タブからApps Scriptを選択する。

    Slack_app_page

    この書き方をコンテナバインドスクリプトと言うらしい。今回はここで記述していく。
    ここでも当然のようにChatGPT先生に教えてもらいながら進めていった。先生、GASも強いっぽい。


    openaiのAPIキー取得

    ChatGPTのAPIを使用するために、APIキーを取得する。
    openai社のWebサイトに利用登録(Googleアカウントでも登録可)し、 画面右上のView API KeysからAPIキーを取得。
    なお、APIキーはGASの設定画面→スクリプト プロパティで環境変数に入れることが可能。
    今回は友人に配布する際に使いやすいように、API_keyシートに直書きする仕様にした。

    openai

    GASでChatGPTへリクエストを投げる

    ChatGPTにやり方を聞いたところ、多分GPT3時代のモデルdavinciへのリクエスト方法を返してきた。 ネットで調べながらちょい変更して実装。

    function chatGPTSimpleResponce() {
      let sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('API_key');
      const apikey = sheet.getRange('A1').getValue();
        // OpenAIのエンドポイント
      const apiUrl = 'https://api.openai.com/v1/chat/completions';
      // System パラメータ
      const systemPrompt = '日本語で回答してください'
      // 入出力シート
      const sheetPrompt = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('test');
    
      // 送信メッセージを定義
      let messages = [{'role': 'system', 'content': systemPrompt},];
    
      // 入力情報を取得
      let promptCell = sheetPrompt.getRange('A1');
      let prompt =promptCell.getValue();
    
      // 質問内容を追加
      messages.push({'role': 'user', 'content': prompt});
    
      // パラメータ設定
      const requestBody = {
        'model': 'gpt-3.5-turbo',
        'temperature': 1.0,
        'max_tokens': 1000,
        'messages': messages
      }
    
      // リクエスト送信
      const request = {
        method: "POST",
        muteHttpExceptions : true,
        headers: {
          'Content-Type': 'application/json',
          'Authorization': 'Bearer ' + apikey,
        },
        payload: JSON.stringify(requestBody),
      }
    
      // 回答先のセルを指定
      responseCell = sheetPrompt.getRange('B1');
      //OpenAIのChatGPTにAPIリクエストを送り、結果を変数に格納
      const response = JSON.parse(UrlFetchApp.fetch(apiUrl, request).getContentText());
      // ChatGPTのAPIレスポンスをセルに記載
      responseCell.setValue(response.choices[0].message.content);
    }
    

    スプレッドシートの「API_key」シートのA1セルに書かれたキーを読み取って、 「test」というシートのA1セルに書かれたプロンプトへの回答をB1セルへ書き出す、というコード。
    一応API利用は有償なので注意。
    1,000入力トークンあたり0.015ドル、1,000出力トークンあたり0.002ドルと激安で、しかもアカウント作成時に18ドルの無料枠(使用期限3か月) が与えられるので枠内でもかなり遊べる。
    今回は会話を貯めていく必要は無いため、リクエスト部分はこれでほぼ完成。後でシート名を引数に持たせて改変した。

    8つの回答をもらうためのプロンプト作成

    激安とはいえ、64個の目標を考えてもらうために64回リクエストを投げるわけにはいかない。
    (GASの制限で、スクリプト実行時間6分迄となっているのにも多分ひっかかるし。)
    せっかくならなるべく実用的な思考の展開をしてもらいたいので、大谷選手も実践したというオープンウィンドウ64(以下OW64)を参考にして プロンプトを作成してみた。

    まず、立てた目標に対してそれを実践するために必要な要素8つ、OW64では基礎思考という。を設定するプロンプト

    const systemPrompt = `
        以下に与える目標について、これを実現するために達成すべき8つの基礎思考を考えて改行区切りで出力してください。
        1から8の番号を頭に付けて出力し、この8行以外の内容は絶対に一切出力しないでください。
        また、8つの基礎思考には精神面、技術面、体力面、生活面での内容を含めてください。
    `

    好みに合わせてもっと具体的に設定するのもありだと思う。その際はプロンプトの temperature(回答の自由度みたいなこと)の値も含めて微調整すると良いかもしれない。
    私にはこの設定で概ねいい感じのが帰ってきた。
    一つのセルに回答を返すが、先で分割させたいので、8行だけきっちり回答してもらいたい旨を強めに指定した。


    基礎思考8つをもとに、それぞれを達成する為に実践したい目標、OW64では実践思考という。を設定するプロンプト

    const systemPrompt = `
        以下に掲げる目標を達成するために具体的に私が実践するためのタスクを8つ考えて改行区切りで出力してください。
        回答については、1から8の番号を頭に付けて出力し、この8行以外の内容は絶対に一切出力しないでください。
    `

    絶対、とか一切、とか入れておくのは結構意味が大きい。ここまで言っても稀に他のことも出力してしまうが。



    もらった回答をGASで分割

    最終的には一つのセルに一つの回答で見せたいため、セルの内容を改行区切りで分割して、隣の列に書き出すGASを用意する。
    以下のような表形式にして、A1に目標を入力、B1にChatGPTの回答の基礎思考(改行区切り8つ)を書き出して、それをC列へ書き出す。

    A列 B列 C列 D列 E列
    1設定した目標8つの基礎思考基礎思考18つの実践思考1実践思考1-1
    2実践思考1-2
    3実践思考1-3
    4実践思考1-4
    5実践思考1-5
    6実践思考1-6
    7実践思考1-7
    8実践思考1-8
    9基礎思考28つの実践思考2実践思考2-1
    10実践思考2-2
    中略
    64実践思考7-7
        function splitStringToCells() {
          let sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('入出力シート');
          let value = sheet.getRange("B1").getValue();
    
          // 文字列を改行で分割
          let values = value.split("\n");
    
          // C1~8行おきに書き出し
          for (let i = 0; i < values.length; i++) {
            sheet.getRange("C" + (i*8 + 1)).setValue(values[i]);
          }
        }

    C列に8行おきに書き出した目標に対して同様の操作を行い、D列へChatGPTの回答→E列へ分割書き出しする。 後は別シートを作って、内容をリンクさせれば完成。



    コードまとめ

    上記を踏まえて、

  • 目標に対して8つの基礎思考をChatGPTに考えてもらいB1セルへ書き出し
  • 行ごとに分割してC列へ書き出し
  • それぞれの基礎思考について8つの実践思考をD列に書き出し
  • 行ごとに分割してE列へ書き出す
  • コード全文は以下

        //この関数で基礎思考8つを作成、分割
        function chatGPTRequestBasicthink() {
          let sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('API_key');
          const apikey = sheet.getRange("A1").getValue();
            // OpenAIのエンドポイント
          const apiUrl = 'https://api.openai.com/v1/chat/completions';
          // System パラメータ
          const systemPrompt = `
        以下に与える目標について、これを実現するために達成すべき8つの基礎思考を考えて改行区切りで出力してください。1から8の番号を頭に付けて出力し、この8行以外の内容は絶対に一切出力しないでください。また、8つの基礎思考には精神面、技術面、体力面、生活面での内容を含めてください。
        `
          // 入出力シート
          const sheetPrompt = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('入出力シート');
    
          // 送信メッセージを定義
          let messages = [{'role': 'system', 'content': systemPrompt},];
    
    
          // 入出力シートからの入力情報を取得
          let promptCell = sheetPrompt.getRange("A1");
          let prompt = "目標:" + promptCell.getValue();
    
          // 質問内容を追加
          messages.push({'role': 'user', 'content': prompt});
    
          // パラメータ設定
          const requestBody = {
            'model': 'gpt-3.5-turbo',
            'temperature': 1.0,
            'max_tokens': 1000,
            'messages': messages
          }
    
          // 送信内容を設定
          const request = {
            method: "POST",
            muteHttpExceptions : true,
            headers: {
              'Content-Type': 'application/json',
              'Authorization': 'Bearer ' + apikey,
            },
            payload: JSON.stringify(requestBody),
          }
    
          // 回答先のセルを指定
          responseCell = sheetPrompt.getRange("B1");
          //OpenAIのChatGPTにAPIリクエストを送り、結果を変数に格納
          const response = JSON.parse(UrlFetchApp.fetch(apiUrl, request).getContentText());
          // ChatGPTのAPIレスポンスをセルに記載
            responseCell.setValue(response.choices[0].message.content);
    
          //行分割
          splitStringToCells()
          }
    
    
        function splitStringToCells() {
          let sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('入出力シート');
          let value = sheet.getRange("B1").getValue();
    
          // 文字列を改行で分割
          let values = value.split("\n");
    
          // C1~8行おきに書き出し
          for (let i = 0; i < values.length; i++) {
            sheet.getRange("C" + (i*8 + 1)).setValue(values[i]);
          }
        }
    
        function chatGPTRequestPractical(input_cell,output_cell) {
          let sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('API_key');
          const apikey = sheet.getRange("A1").getValue();
            // OpenAIのエンドポイント
          const apiUrl = 'https://api.openai.com/v1/chat/completions';
          // System パラメータ
          const systemPrompt = `
        以下に掲げる目標を達成するために具体的に私が実践するためのタスクを8つ考えて改行区切りで出力してください。
        回答については、1から8の番号を頭に付けて出力し、この8行以外の内容は絶対に一切出力しないでください。
        `
          // 入出力シート
          const sheetPrompt = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('入出力シート');
    
          // 送信メッセージを定義
          let messages = [{'role': 'system', 'content': systemPrompt},];
    
    
          // 入出力シートからの入力情報を取得
          let promptCell1 = sheetPrompt.getRange("A1");
          let goal_prompt =promptCell1.getValue();
          let promptCell2 = sheetPrompt.getRange(input_cell);
          let prompt = "目標:" + goal_prompt +"という大目標を達成するため、"+ promptCell2.getValue();
    
          // 質問内容を追加
          messages.push({'role': 'user', 'content': prompt});
    
          // パラメータ設定
          const requestBody = {
            'model': 'gpt-3.5-turbo',
            'temperature': 1.0,
            'max_tokens': 1000,
            'messages': messages
          }
    
          // 送信内容を設定
          const request = {
            method: "POST",
            muteHttpExceptions : true,
            headers: {
              'Content-Type': 'application/json',
              'Authorization': 'Bearer ' + apikey,
            },
            payload: JSON.stringify(requestBody),
          }
    
          // 回答先のセルを指定
          responseCell = sheetPrompt.getRange(output_cell);
            //OpenAIのChatGPTにAPIリクエストを送り、結果を変数に格納
            const response = JSON.parse(UrlFetchApp.fetch(apiUrl, request).getContentText());
            // ChatGPTのAPIレスポンスをセルに記載
            responseCell.setValue(response.choices[0].message.content);
          }
    
        //この関数で実践施行8つを作成、分割
        function MakePracticalThink(){
            for (let i = 0; i < 8; i++) {
              let input_cell = "C"+ (i*8 + 1)
              let output_cell = "D"+ (i*8 + 1)
              chatGPTRequestPractical(input_cell,output_cell);
          }
          make_Item()
        }
        //引数に与えたセルを改行ごとに分割し、E列の同行数から8行に書き出す。
        function splitStringToCells_with_args(get_cell) {
          let sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('入出力シート');
          let value = sheet.getRange(get_cell).getValue();
            // 正規表現で非数字の文字を除去
          let numStr = get_cell.match(/\d+/)[0];
          let startRow = parseInt(numStr, 10);
          // 文字列を改行で分割
          let values = value.split("\n");
    
          // E列の与えたセルの行番号から8行にわたり書きだす。
          for (let i = 0; i < values.length; i++) {
            sheet.getRange("E" + (startRow + i)).setValue(values[i]);
          }
        }
    
        function make_Item(){
            for (let i = 0; i < 8; i++) {
              let target_cell = "D"+ (i*8 + 1)
              splitStringToCells_with_args(target_cell);
          }
        }
    
        function chatGPTSimpleResponce() {
          let sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('API_key');
          const apikey = sheet.getRange("A1").getValue();
            // OpenAIのエンドポイント
          const apiUrl = 'https://api.openai.com/v1/chat/completions';
          // System パラメータ
          const systemPrompt = '日本語で回答してください'
          // 入出力シート
          const sheetPrompt = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('test');
    
          // 送信メッセージを定義
          let messages = [{'role': 'system', 'content': systemPrompt},];
    
          // 入力情報を取得
          let promptCell = sheetPrompt.getRange("A1");
          let prompt =promptCell.getValue();
    
          // 質問内容を追加
          messages.push({'role': 'user', 'content': prompt});
    
          // パラメータ設定
          const requestBody = {
            'model': 'gpt-3.5-turbo',
            'temperature': 1.0,
            'max_tokens': 1000,
            'messages': messages
          }
    
          // リクエスト送信
          const request = {
            method: "POST",
            muteHttpExceptions : true,
            headers: {
              'Content-Type': 'application/json',
              'Authorization': 'Bearer ' + apikey,
            },
            payload: JSON.stringify(requestBody),
          }
    
          // 回答先のセルを指定
          responseCell = sheetPrompt.getRange("B1");
          //OpenAIのChatGPTにAPIリクエストを送り、結果を変数に格納
          const response = JSON.parse(UrlFetchApp.fetch(apiUrl, request).getContentText());
          // ChatGPTのAPIレスポンスをセルに記載
          responseCell.setValue(response.choices[0].message.content);
        }
    

    基礎思考と実践思考は別関数に設定した。
    基礎思考が作られた時点で参考にして自分好みに書き換えてから実践思考を作成してもらうとより実用的に使えると思う。
    また、基礎思考から実践思考を作らせる時は、元の目標から離れて着地することを防ぐために元の目標もプロンプトに盛り込んだ。


    mandara_demo2

    使ってみて

    楽しい!なかなか自分が思いつかない方面の深堀りをしてくれたりもするので、 一回これを眺めてから自分でOW64を作ってみる、という使い方もありだと思う。
    こういう思考系のツール×ChatGPTという組み合わせはいろいろ可能性ありそうだな~と思った。 GASだとスプレッドシートでいろいろ装飾もできるので、見せ方も工夫出来て楽しい。
    最近買ったこの本 が面白いので、他にもこんなのが作れないか考え中です。