GPT-4対応!GASでChatGPT LINE Botを作る【Messaging API】

パソコンの小ワザ

この記事では、OpenAI APIを使用し、ChatGPTを搭載したLINE BotをGoogle Apps Script(GAS)で自作する方法について解説する。

複数ターンの会話履歴考慮や、最新モデル「GPT-4」にも対応できるよう設計している。

既に100番煎じくらいだと思うが、イメージはこんな感じ↓

スポンサーリンク
スポンサーリンク

準備

ChatGPT LINE BotをGASで作成するためには、以下の3つのアカウントが必要。

OpenAI API Keyを取得

OpenAI Platformの管理画面から、API Keyを発行する。

「Create new secret key」をクリックし、出てきた文字列をメモしておく。

最初の3ヶ月間は、18ドル分のクレジットが付与されているため、ある程度無料で楽しめるだろう。

LINE Developersの登録

LINE Developersコンソールに、LINEアカウントでログインする。初回は開発者名とメールアドレスを求められるので、入力して開発者アカウントを作成する。

プロバイダー・チャネル作成

次に、プロバイダーを作成する。名前は何でもOK。

チャネル設定よりMessagins APIを選択し、新規チャネルの設定に進む。

各項目を埋める。チャネル名はボットのアカウント名となるので、それっぽい名称にしておくと良し。

Messaging API設定タブより「チャネルアクセストークン(長期)」を発行する。

下の方にある

API Keyと同様に後で必要になるので、こちらもメモしておく。

Google Apps Script

今回は、スプレッドシートをDB的に利用し、設定や応答を管理する。こちらより新規ファイルを作成。

シートを2つ用意し、1つめをData、2つめをSettingsとしておく。

SettingsのB列に、設定を入力する。

  • モデル … 「gpt-3.5-turbo」または「gpt-4」
  • ボットのキャラクター … AIにロールプレイングをさせる時の口調や条件等
  • 最大ターン数 … 過去何ターンまでの会話を記憶するか

最大ターン数は、「プロンプト+応答」のセットを1としている。数値を大きくすれば、より多くの会話履歴を考慮した回答が返ってくるが、その分高コストとなるため注意が必要だ。

GPT-4のAPIは、2023年7月6日に一般開放が発表された。ただし、記事執筆時点では、一度でも支払い履歴のあるユーザーに限られる。年内には新規登録ユーザーも利用可能になるとのこと。参考:OpenAI

画面上部メニューの拡張機能 > Apps Scriptを押下すると、以下の画面が出現。

ここにコードを書いていく。

スポンサーリンク

コード全文

以下のGASコードをコード.gsにコピペする。

OPENAI_APIKEYLINE_ACCESS_TOKENは、先程取得した文字列に置き換える。

const OPENAI_APIKEY = "sk-xxxxxxxxxxxx"; // 自身のAPI Keyに書き換える
const LINE_ACCESS_TOKEN = "Qbxxxxxxxxxxxx="; // 自身のアクセストークンに書き換える

function doPost(e) {
  let fromLineData = e.postData.contents;
  let receiveData = JSON.parse(fromLineData);
  // text部分を取り出す
  let receiveMsg = receiveData.events[0].message.text;
  // replyTokenを取り出す
  let replyToken = receiveData.events[0].replyToken;

  // ============= debug =============
  let sheetSettings = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Settings");
  sheetSettings.getRange(4, 2).setValue(receiveMsg);
  sheetSettings.getRange(5, 2).setValue(fromLineData);
  //==========================

  // myFunctionを呼ぶ
  let replyMessage = myFunction(receiveMsg);

  // reply関数を呼ぶ
  reply(replyToken, replyMessage);

  // forgotData関数を呼ぶ
  forgotData();
}

// LINEへの返信
function reply(replyToken, replyMessage){
  let replyUrl = "https://api.line.me/v2/bot/message/reply";
  let contents = {
          replyToken: replyToken,
          messages: [{ type: 'text', text: replyMessage }],
  };
  let options = {
    method: 'post',
    contentType: 'application/json',
    headers: {
      Authorization: 'Bearer ' + LINE_ACCESS_TOKEN
    },
    payload: JSON.stringify(contents)
  };
  UrlFetchApp.fetch(replyUrl, options);
}

function myFunction(receiveMsg) {
  // スプレッドシートの読み込み
  let spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  // シートの選択
  let sheetSettings = spreadsheet.getSheetByName("Settings");
  // セルの選択
  let rangeSettings = sheetSettings.getRange("B2");
  // セルの値を取得する(ボットのキャラクター設定)
  let botCharacter = rangeSettings.getValue();
  // セルの選択
  rangeSettings = sheetSettings.getRange("B1");
  // セルの値を取得する(モデル名)
  let model = rangeSettings.getValue();

  // setCellValueUserPrompt関数を呼び出す
  setCellValueUserPrompt(receiveMsg);

  // fncCreateMessage関数を呼び出す
  let createdMessage = fncCreateMessage(botCharacter);

  // fncCallApi関数を呼び出す
  let botAnswer = fncCallApi(createdMessage, model);

  // setCellValue関数を呼び出す
  setCellValue(botAnswer);

  console.log(botAnswer);
  return(botAnswer);
}

// スプレッドシートに値を書き込む関数(ユーザーの入力)
function setCellValueUserPrompt(userPrompt) {
  let sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Data");
  let columnB = sheet.getRange("B:B").getValues();
  for (let i = 0; i < columnB.length; i += 2) {
    if (columnB[i][0] == "") {
      sheet.getRange(i + 1, 2).setValue(userPrompt);
      break;
    }
  }
}

// スプレッドシートに値を書き込む関数(ボットの応答)
function setCellValue(botAnswer) {
  // 現在アクティブなシートを取得する
  let sheet = SpreadsheetApp.getActiveSheet();
  // 最終行数を取得する
  let lastRow = sheet.getLastRow();
  // 2列目の範囲オブジェクトを取得する
  let range = sheet.getRange(2, 2, lastRow, 1);
  // 2列目の値を取得する
  let values = range.getValues();

  // 2列目の値を順番に処理する
  for (let i = 0; i < values.length; i++) {
    // 空セルかどうかを判断する
    if (values[i][0] === "") {
      // 奇数行かどうかを判断する
      if ((i + 2) % 2 === 1) {
        // 繰り返し処理をスキップする
        continue;
      }
      // 空セルの範囲オブジェクトを取得する
      let cell = sheet.getRange(i + 2, 2);
      // セルに値を代入する
      cell.setValue(botAnswer);
      // 繰り返し処理を終了する
      break;
    }
  }
}

// 設定した最大ターン数に応じて、スプレッドシートの値を上に詰める
function forgotData() {
  // シートの取得
  let sheetSettings = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Settings");
  // 設定された最大ターン数を取得
  let maxNumberOfTurns = sheetSettings.getRange("B3").getValue();
  // シートの取得
  let sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("data");
  // 最終行数の取得
  let lastRow = sheet.getRange("B:B").getValues().filter(String).length;
  
  // 最大ターンを超える場合
  if (lastRow > maxNumberOfTurns * 2) {
    for (let i = 3; i <= lastRow; i++) {
      let value = sheet.getRange(i, 2).getValue();
      sheet.getRange(i - 2, 2).setValue(value);
    }
    // 指定行数以上の値を削除
    sheet.getRange(maxNumberOfTurns * 2 + 1, 2, 2, 1).clearContent();
  }
}

// メッセージ部分を作成する関数
function fncCreateMessage(botCharacter) {
  // スプレッドシートの読み込み
  let spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  // シートの選択
  let sheetData = spreadsheet.getSheetByName("Data");

  // 最終行数の取得
  let lastRow = sheetData.getRange("B:B").getValues().filter(String).length;

  // プロンプト・応答を格納する配列
  let arrMessage = [];  
  
  // 行数分だけ要素を配列に追加する
  for (let i = 1; i <= lastRow; i++) {
    // セルの選択
    let rangeData = sheetData.getRange("B" + i);
    // セルの値を取得する
    let cellVal = rangeData.getValue();
    // 配列の末尾に要素を追加
    arrMessage.push(cellVal);
  }

  // message配列 1項目めはsystemメッセージ(キャラクター設定)
  let message = [{"role": "system", "content": botCharacter }];

  // メッセージ部分を作成
  for (let i = 0; i < arrMessage.length; i++) {
    // 奇数行の場合は、roleを"assistant"に設定し、それ以外の場合は、"user"に設定する
    let role = (i % 2 === 1) ? "assistant" : "user";
    // message配列に新しいオブジェクトを追加する
    message.push({
      "role": role,
      "content": arrMessage[i]
    });
  }
  console.log(message);
  // 作成したメッセージ(配列)を返す
  return message;
}

// OpenAI APIを呼び出す関数
function fncCallApi(createdMessage, model) {
  let requestUrl = "https://api.openai.com/v1/chat/completions";
  let headers = {
    "Content-Type": "application/json",
    "Authorization": "Bearer " + OPENAI_APIKEY,
  };
  let payload = {
      "model": model,
      "messages": createdMessage
  };
  let options = {
    "method": "POST",
    "headers": headers,
    "payload": JSON.stringify(payload)
  };
  // UrlFetchAppでAPIを呼び出し、レスポンスを取得
  let response = UrlFetchApp.fetch(requestUrl, options).getContentText();
  // レスポンスをJSONオブジェクトに変換
  let objResponse = JSON.parse(response);
  // choices[0]のmessage.contentを返す
  return objResponse.choices[0].message.content;
}

半分くらいはChatGPTに書かせたので、所々冗長だったりするかも。

あと、エラーハンドリングが全く無いので、追加したほうが望ましい。

デプロイ

GAS画面右上のデプロイより「新しいデプロイ」を選択。

種類の選択 > ウェブアプリ

アクセスできるユーザーを全員に変更し、デプロイする。

初回は警告画面が出るが、気にせずに全ての権限を付与する。

「デプロイを更新しました」という画面に表示されているURLをコピーする。

スポンサーリンク

Webhook設定

次に、LINE Developersコンソール側でWebhook URLを設定する。

先程コピーしたURLをWebhook URL欄に貼り付ける。「Webhookの利用」もオンにしておく。

表示されているQRコードから友だち登録すると、ボットを使用可能になる。

LINE公式アカウント機能の「応答メッセージ」は無効化しておくと良いだろう。

ボットに話しかけて、返信が来れば成功だ!

お疲れ様でした。

コメント

タイトルとURLをコピーしました