-----
・追記(2023-4-4)
system ロールへの指示を複数登録、任意に使用できるように修正しました。
・追記(2023-4-14)
文章のランダムさを制御する temperature パラメータを調整できるように修正しました。
・追記(2024-5-17)
GPT-4o API による画像処理機能を追加したバージョンを作りました → 「GAS で GPT-4o API を使った画像処理ができる LINE bot を作る」
-----
先日作った「GAS で ChatGPT の API を利用した簡単な LINE bot を作ってみる」に引き続き、複数回のやりとりができるバージョンを作ってみました。
やりとりは Google スプレッドシートに保存されます。
「chat」シート
文頭に「続き)」がない場合、やりとりは削除されます。
また system ロールへの指示も可能です。system ロールへの指示はスプレッドシートに直接書き込んでおきます。
「system」シート
watchword は system ロールへの指示を使用する際の合言葉です。文頭に「'watchword'!」の形式で書くことで使用できます。
例えば、「要約!」を付けると「3行で要約してください」という指示がされます(実際には英訳して指示)。
「昔話!」なら「与えられた題材から3行で昔話を作ってください」という指示がされ、簡単な昔話を作ってくれます。
さらに、文章のランダムさを制御する temperature パラメータの値を保存しておく「temperature」シートを作成しておきます。中身は空白で大丈夫です。
質問を始める際に、文末に「(厳密」もしくは「(創造」を付けることで使用できます。「(厳密」であればランダムさは「0」、「(創造」であれば「1」になります。何も付けない場合は「0.5」です。
「続き)」を使ってやりとりを継続すると、ランダムさも引き継がれます。
LINE Messaging API チャンネルの作成とアクセストークンの取得。及び、OpenAI の API キー取得は済んでる想定です。
それぞれ、GAS の「プロジェクトの設定」よりスクリプト プロパティとして追加・保存しておきます。プロパティ名は「Line_key」と「OpenAI_key」としています。
以下、コードになります。
const spreadsheet = SpreadsheetApp.openById('スプレッドシート ID'); const chatSheet = spreadsheet.getSheetByName('chat'); let chatLastRow = chatSheet.getLastRow(); let chatRange = chatSheet.getRange(2, 1, chatLastRow, 2); const systemSheet = spreadsheet.getSheetByName('system'); const systemLastRow = systemSheet.getLastRow(); const systemWatchwordRange = systemSheet.getRange(2, 1, systemLastRow - 1, 1); const systemContentRange = systemSheet.getRange(2, 2, systemLastRow - 1, 1); const tempSheet = spreadsheet.getSheetByName('temperature'); const tempRange = tempSheet.getRange(1, 1); function doPost(e) { try { const replyToken = getReplyToken(e); const requestMessage = getRequestMessage(e); const openAiParams = generateOpenAIParams(requestMessage); const response = UrlFetchApp.fetch('https://api.openai.com/v1/chat/completions', openAiParams); const resData = JSON.parse(response.getContentText()); const replyMessage = resData.choices[0].message.content; setAssistantMessage(replyMessage); // スプレッドシートに'role': 'assistant'で格納 const translatedMessage = LanguageApp.translate(replyMessage, 'en', 'ja'); const replyContent = `${requestMessage[requestMessage.length - 1]['content']}\n-----\n${translatedMessage}\n\n${replyMessage}`; const linePayload = generateLinePayload(replyToken, replyContent); sendLineMessage(linePayload); } catch (error) { console.error(`An error occurred: ${error}`); } } // pushメッセージからreplyTokenを取得する関数 function getReplyToken(e) { const event = JSON.parse(e.postData.contents).events[0]; return event.replyToken; } // リクエストメッセージを生成する関数 function getRequestMessage(e) { const event = JSON.parse(e.postData.contents).events[0]; const message = event.message; let messageContent = message.type === 'text' ? message.text : ''; // リクエストメッセージの配列を生成 const systemWatchwordArry = systemWatchwordRange.getValues().flat(); const systemContentArry = systemContentRange.getValues().flat(); let systemWatchword = ""; let systemContent = ""; if (messageContent.startsWith('続き)')) { messageContent = LanguageApp.translate(messageContent.replace('続き)', ''), '', 'en'); chatSheet.getRange(chatLastRow + 1, 1, 1, 2).setValues([['user', messageContent]]); } else { // temperatureを取得してスプレッドシートにセット if (messageContent.endsWith('(創造')) { messageContent = messageContent.replace('(創造', ''); tempRange.setValue(1); } else if (messageContent.endsWith('(厳密')) { messageContent = messageContent.replace('(厳密', ''); tempRange.setValue(0); } else { tempRange.setValue(0.5); } // systemロールの読み込み for (let i = 0; i < systemWatchwordArry.length; i++) { if (messageContent.startsWith(systemWatchwordArry[i] + '!')) { systemWatchword = systemWatchwordArry[i] + '!'; systemContent = LanguageApp.translate(systemContentArry[i], '', 'en'); break; } } messageContent = LanguageApp.translate(messageContent.replace(systemWatchword, ''), '', 'en'); chatRange.clear(); if (systemContent) { chatSheet.getRange(2, 1, 2, 2).setValues([['system', systemContent], ['user', messageContent]]); chatRange = chatSheet.getRange(2, 1, 2, 2); } else { chatSheet.getRange(2, 1, 1, 2).setValues([['user', messageContent]]); chatRange = chatSheet.getRange(2, 1, 1, 2); } } let requestMessage = []; if (systemContent) { requestMessage.push({'role': 'system', 'content': systemContent}); } let values = chatRange.getValues(); for(let i = 0; i < values.length; i++) { requestMessage.push({'role': values[i][0], 'content': values[i][1]}); } return requestMessage; } // OpenAIのリクエストパラメータを生成する関数 function generateOpenAIParams(requestMessage) { const openAiHeaders = { 'Authorization': `Bearer ${PropertiesService.getScriptProperties().getProperty('OpenAI_key')}`, 'Content-type': 'application/json', 'X-Slack-No-Retry': 1 }; const openAiPayload = { 'model': 'gpt-3.5-turbo', // 使用するモデル 'max_tokens': 1024, // 生成する文章の最大トークン数 'temperature': tempRange.getValue(), // 生成された文章のランダムさを制御するパラメータ。値が高いほど、よりランダムな文章が生成される 'messages': requestMessage // 生成する文章の元になるプロンプト }; return { 'method': 'POST', 'headers': openAiHeaders, 'payload': JSON.stringify(openAiPayload), }; } // アシスタントの返答をスプレッドシートに格納 function setAssistantMessage(translatedMessage) { chatLastRow = chatSheet.getLastRow(); chatSheet.getRange(chatLastRow + 1, 1, 1, 2).setValues([['assistant', translatedMessage]]); } // LINEに返信するメッセージを生成する関数 function generateLinePayload(replyToken, replyMessage) { return { 'method': 'post', 'headers': { 'Content-Type': 'application/json', 'Authorization': `Bearer ${PropertiesService.getScriptProperties().getProperty('Line_key')}`, }, 'payload': JSON.stringify({ 'replyToken': replyToken, 'messages': [{ 'type': 'text', 'text': replyMessage, }], }), }; } // LINEにメッセージを送信する関数 function sendLineMessage(linePayload) { const response = UrlFetchApp.fetch('https://api.line.me/v2/bot/message/reply', linePayload); const responseCode = response.getResponseCode(); if (responseCode !== 200) { console.error(`LINE API returned an error: ${responseCode}`); } }
1行目の「スプレッドシート ID」は書き換えてください。
やりとりの保存用と system ロールへの指示用シートの名前はそれぞれ「chat」と「system」としています。
コード作成後にウェブアプリとして公開、URL を LINE Messaging API チャンネルで Webhook として設定して完成です。
***
会話と system ロールへの指示ができると、また一段と活用法が広がる気がしますね。
無料枠を使い切った後も自分(家族)用の便利 bot として利用するのはありかもと思っています。コストとしても月に数十〜数百円程度なら許容範囲です。Bing やブラウザから ChatGPT を使うのが面倒なちょっとした時に重宝しそうです。
ちなみに今回も ChatGPT にコードの作成を手伝ってもらったのですが、ときどき存在しないメソッドを回答され混乱しました。信用しきってはだめですね(笑)。
詳解! Google Apps Script完全入門 [第3版]
by SimpleImageLink
Twitter (@nkkmd) 日々更新中です。