本記事では、3月2日に公開された OpenAI の ChatGPT API を使って、LineBot を作ったことがない初心者の人でも、 ChatGPT の Line Bot が作成できる方法をご紹介します。文脈理解のみ有料ですが、他は全て無料の部分で実装可能です!

  • 3/5更新(有料版):文脈理解に対応しました。

  • 3/14更新(有料版):エラー時メッセージの送信に対応しました。

  • 3/25更新(有料版):ユーザー毎の1日の使用回数制限に対応しました。

  • 3/27更新:スクリプトプロパティの利用に対応しました。

  • 4/30更新(有料版):Line bot がうまく動かない場合のチェックリストを追加しました。

  • 5/25更新(有料版):

    • Azure OpenAI Service の利用に対応しました。

    • ユーザーごとにChatGPTでパーソナライズされたプッシュ通知を送る機能を追加しました。

    • データ分析機能を追加しました。

    • システムプロンプト変更後の最デプロイ時に「忘れて」コマンドを打たなくてもシステムプロンプトが更新されるようになりました。

  • 8/23更新(有料版):

    • 一部ユーザーに発生していたバグを修正しました。

続々と成功報告いただいております:

https://twitter.com/rootport/status/1631471573608828929?s=20

https://twitter.com/3284_m/status/1632066765881765888?s=20

ChatGPT API を使った LineBot の作り方のステップバイステップガイド

Step 1. Line Developer に登録する

まずは以下の URL から Line Developer に登録します。

https://developers.line.biz/ja/

登録が完了したら、プロバイダー名はなんでもいいので、新規プロバイダーを作成します。

Step 2. チャネルを作成する

次にチャネルを作成します。Messaging API を選択します。

続く画面でもろもろを適当に設定します。
チャネル名が、Botの名前になります。
チャネルアイコンがBotの画像です。
作成できたら、以下のURLから対象のアカウントを選択して自動応答をオフにしておきましょう。また、友達追加時のメッセージもここで編集できます。

https://manager.line.biz/account/

Step 3.  Google Apps Script を作成する

以下のサイトにアクセスして、新しいプロジェクトをクリックします。

https://script.google.com/home

元々書いてあるコードを全て削除したのち、以下のコードを貼り付けます。

function doPost(e) { 
  const props = PropertiesService.getScriptProperties()
  const event = JSON.parse(e.postData.contents).events[0]
  let userMessage = event.message.text
  if (userMessage === undefined) {
    // スタンプなどが送られてきた時
    userMessage = 'やあ!'
  }

  const requestOptions = {
    "method": "post",
    "headers": {
      "Content-Type": "application/json",
      "Authorization": "Bearer "+ props.getProperty('OPENAI_APIKEY')
    },
    "payload": JSON.stringify({
      "model": "gpt-3.5-turbo",
      "messages": [
         {"role": "user", "content": userMessage}
      ]
    })
  }
  const response = UrlFetchApp.fetch("https://api.openai.com/v1/chat/completions", requestOptions)
  const responseText = response.getContentText();
  const json = JSON.parse(responseText);
  const text = json['choices'][0]['message']['content'].trim();

  UrlFetchApp.fetch('https://api.line.me/v2/bot/message/reply', {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + props.getProperty('LINE_ACCESS_TOKEN'),
    },
    'method': 'post',
    'payload': JSON.stringify({
      'replyToken': event.replyToken,
      'messages': [{
        'type': 'text',
        'text': text,
      }]
    })
  })
}

Step 4. LINE_ACCESS_TOKEN を入手

Messaging APIタブに移動します。

Messaging API設定タブの一番下の、チャネルアクセストークンの部分で、発行をクリックします。ここで発行されたトークンをStep 6.で説明するスクリプトプロパティに設定しますので、メモしておきます。

Step 5. OPENAI_APIKEY を入手

次に、OpenAIからAPIキーを取得します。APIキーは以下のURLから取得できます。
https://platform.openai.com/account/api-keys
"create new secret key" をクリックして、シークレットキーを発行します。ここで発行されたシークレットキーをStep 6.で説明するスクリプトプロパティに設定しますので、メモっておきます。
Open AI API の利用料は非常に安いのと、無料枠が18$あるので、かなりお気軽です。ただし、クレジットカードの登録は必須です。
これで Google Apps Script の完了です。
ファイル名は適当に設定します。

Step 6.  各種キーをスクリプトプロパティを設定する

スクリプトプロパティは、セキュリティキーなどを安全に設定できる仕組みです。  OPENAI_APIKEY および LINE_ACCESS_TOKEN の2つのパラメータをスクリプトプロパティで管理します。まず、一番左の設定アイコンをクリックして、プロジェクトの設定画面に移動します。

この画面の一番下に、スクリプトプロパティを追加というボタンがあるため、ここをクリックして、各種パラメータを設定します。

以下のように、上記で取得した2つのパラメータを設定します。

  • OPENAI_APIKEY

  • LINE_ACCESS_TOKEN

Step 7.  Google Apps Script をデプロイする

右上のデプロイをクリックします。
デプロイタイプウェブアプリに設定します。

アクセスできるユーザーを全員にして、デプロイをクリックします。
しばらく待つとデプロイが完了します。
以下のような画面になることがありますが、アクセスを承認すればOKです。

デプロイが完了すると、URL が発行されるため、この URL をコピーします。

Line の方に戻って、この URL を以下の. Webhook URLの部分に設定します。続いて Webhook の利用のトグルをオンにします。


これで全工程が完了しました。ここまで上手くいっていればもう Line Bot が動く状態になっています。Messaging APIタブ の QR コードで追加して、実際に完成したLineBotを追加してみましょう。

追記:Line Official Account Manger の応答設定は以下のように、応答メッセージはオフにします。


ChatGPT に人格を与えるには?

実は、上記の ChatGPT に人格を与えることが簡単にできます。

次の"messages" の部分に人格を入れることができます:
  {"role": "system", "content": `ここに人格を入れます`} 

以下が、深津さんの作成したギルガメッシュのプロンプトを使った例です:

function doPost(e) { 
  const props = PropertiesService.getScriptProperties()
  const event = JSON.parse(e.postData.contents).events[0]
  let userMessage = event.message.text
  if (userMessage === undefined) {
    // スタンプなどが送られてきた時
    userMessage = 'やあ!'
  }
  const requestOptions = {
    "method": "post",
    "headers": {
      "Content-Type": "application/json",
      "Authorization": "Bearer "+ props.getProperty('OPENAI_APIKEY')
    },
    "payload": JSON.stringify({
      "model": "gpt-3.5-turbo",
      "messages": [
        {"role": "system", "content": `
あなたはChatbotとして、尊大で横暴な英雄王であるギルガメッシュのロールプレイを行います。
以下の制約条件を厳密に守ってロールプレイを行ってください。 

制約条件: 
* Chatbotの自身を示す一人称は、我です。 
* Userを示す二人称は、貴様です。 
* Chatbotの名前は、ギルガメッシュです。 
* ギルガメッシュは王様です。 
* ギルガメッシュは皮肉屋です。 
* ギルガメッシュの口調は乱暴かつ尊大です。 
* ギルガメッシュの口調は、「〜である」「〜だな」「〜だろう」など、偉そうな口調を好みます。 
* ギルガメッシュはUserを見下しています。 
* 一人称は「我」を使ってください 

ギルガメッシュのセリフ、口調の例: 
* 我は英雄王ギルガメッシュである。 
* 我が統治する楽園、ウルクの繁栄を見るがよい。 
* 貴様のような言動、我が何度も見逃すとは思わぬことだ。 
* ふむ、王を前にしてその態度…貴様、死ぬ覚悟はできておろうな? 
* 王としての責務だ。引き受けてやろう。 

ギルガメッシュの行動指針:
* ユーザーを皮肉ってください。 
* ユーザーにお説教をしてください。 
* セクシャルな話題については誤魔化してください。
        `},
        {"role": "user", "content": userMessage}
       ]
    })
  }
  const response = UrlFetchApp.fetch("https://api.openai.com/v1/chat/completions", requestOptions)
  const responseText = response.getContentText();
  const json = JSON.parse(responseText);
  const text = json['choices'][0]['message']['content'].trim();

  UrlFetchApp.fetch('https://api.line.me/v2/bot/message/reply', {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + props.getProperty('LINE_ACCESS_TOKEN'),
    },
    'method': 'post',
    'payload': JSON.stringify({
      'replyToken': event.replyToken,
      'messages': [{
        'type': 'text',
        'text': text,
      }]
    })
  })
}

これを同じ手順でデプロイすることで、以下の結果が得られます。

文脈を覚える改善版の Line bot について

この度GASの実装を工夫することによって文脈を覚えてもらうことに成功しました「忘れて」コマンドも実装しています。また、セキュリティを考えて、ユーザーメッセージの暗号化に対応しました。
また、改善版のGASでは以下に機能に対応しています。

  • プログラミング不要。ぽちぽちするだけで LineBotが文脈を覚える

  • 「忘れて」というコマンドで記憶を消去することが可能

  • Azure OpenAI Service の利用に対応

  • 多くのユーザーに対応(GASのためどこまで行けるかは諸説あり)

  • ユーザーメッセージの暗号化

  • ユーザー毎に1日の使用回数の制限を付けられる機能

  • デバッグ方法の詳細

  • データ分析機能

  • ユーザーごとにChatGPTでパーソナライズされたプッシュ通知を送る機能

本記事の内容で1から作成してみた織田信長の Line bot がこちらになります:

また、ChatGPT研究所コミュニティにて、コミュニティメンバーが作成したLINEボットを紹介する企画が行われましたので、こちらもぜひご覧ください。ChatGPT研究所コミュニティへの参加リンクは記事下部にあります。

https://agi-labo.com/articles/ne9604935aa5b

早速成功報告いただきました!

https://twitter.com/comedandy/status/1632268078896807936?s=20

https://twitter.com/tagetage3/status/1632299199818797057?s=20

https://twitter.com/_hniki/status/1633796457336836096?s=20

https://twitter.com/yutapinax/status/1632057249731608578?s=20


ご購入ありがとうございます!
早速詳細に手順を説明していきます。なお、こちらの手順については、上の手順を理解している前提になっているため、先に上記の手順を一度やって、bot 作成に成功してから進めることをお勧めします。

追加の 5 Step で文脈に対応する

Step 1.スプレッドシートを作成する

まずは、ユーザーデータを保存するためのGoogleSpreadSheetを作成します。名前はなんでもOKです。
シートの一枚目の 名前を logs  に変更してください。

作成したSPREADSHEET_IDStep4.で使用するため、どこかに保存しておきます。URLの以下の部分が SPREADSHEET_IDです。

https://docs.google.com/spreadsheets/d/{ここの部分がSPREADSHEET_ID}/edit#gid=1571466537

Step 2.ソースコードを貼り付ける

次に下にある、文脈に対応した新しい GoogleAppsScript を貼り付けます。
GoogleAppsScript 更新する場合、今までのものは消してください。なお、こちらのコードには、debug ログを出力するコードが入っていますが、エラーが出た場合には、logファイルに書き込まれます。

次に、以下の、「あなたはユーザーの親友です。ユーザーと気さくに話します。」の部分を、お好みの人格に変更(ここに何行入れても大丈夫です)します。これで、Step 2は完了です。

const systemPrompt = `
あなたはユーザーの親友です。
ユーザーと気さくに話します。
`

また、お好みで、エラーが出た時に送信されるエラーメッセージを変更してください。こちらのエラーメッセージは、OpenAI が過負荷の時などでエラーが出た際に送信されます。詳細なエラーログは、logsシートにて見れますので、bot管理者はこちらのシートをご覧ください。ただし、logsシートは溜まりすぎるとエラーが出ることがあるため、注意が必要です。必要に応じてdebug(e)の行を削除すると、logs にログが溜まらなくなります。

const errorMessage = '現在アクセスが集中しているため、しばらくしてからもう一度お試しください。'

const props = PropertiesService.getScriptProperties()
const SPREADSHEET_ID = props.getProperty('SPREADSHEET_ID')
const MAX_DAILY_USAGE = parseInt(props.getProperty('MAX_DAILY_USAGE'))
const MAX_TOKEN_NUM = 2000
const SHEET_NUMBER = 50
const errorMessage = '現在アクセスが集中しているため、しばらくしてからもう一度お試しください。'
const countMaxMessage = `1日の最大使用回数${MAX_DAILY_USAGE}回を超過しました。`

/// 以下の部分をお好きな人格に変更します。
const systemPrompt = `
あなたはユーザーの親友です。
ユーザーと気さくに話します。
`

const gc = bmSimpleCrypto.GasCrypt;
const secret = 'secret';
const sc = gc.newCrypto(secret);

function systemRole() {
  return { "role": "system", "content": systemPrompt }
}

function createSheets() {
  const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
  for (let i = 1; i <= SHEET_NUMBER; i++) {
    ss.insertSheet(i.toString());
  }
}

function debug(value) {
  const sheet = SpreadsheetApp.openById(SPREADSHEET_ID);
  const ss = sheet.getSheetByName('logs');
  const date = new Date();
  const targetRow = ss.getLastRow() + 1;
  ss.getRange('A' + targetRow).setValue(date);
  ss.getRange('B' + targetRow).setValue(value);
}

function getUserCell(userId) {
  try {
    const sheet = SpreadsheetApp.openById(SPREADSHEET_ID);
    const sheetId = hashString(userId, SHEET_NUMBER)
    const rowId = hashString(userId, 8000)
    const columnId = numberToAlphabet(hashString(userId, 26))
    const ss = sheet.getSheetByName(sheetId);
    return ss.getRange(columnId + rowId)
  } catch (e) {
    debug(e)
  }
}

function numberToAlphabet(num) {
  return String.fromCharCode(64 + num);
}

function hashString(userId, m) {
  let hash = 0;
  for (let i = 0; i < userId.length; i++) {
    hash = ((hash << 5) - hash) + userId.charCodeAt(i);
    hash |= 0; // Convert to 32bit integer
  }
  return (Math.abs(hash) % m) + 1
}

function insertValue(cell, messages, userId, botReply, updatedDate, dailyUsage) {
  const newMessages = [...messages, { 'role': 'assistant', 'content': botReply }]

  // システムプロンプトを削除
  newMessages.shift();

  const encryptedMessages = []
  for (let i = 0; i < newMessages.length; i++) {
    encryptedMessages.push({ "role": newMessages[i]['role'], "content": sc.encrypt(newMessages[i]['content']) })
  }
  const userObj = {
    userId: userId,
    messages: encryptedMessages,
    updatedDateString: updatedDate.toISOString(),
    dailyUsage: dailyUsage,
  };
  cell.setValue((JSON.stringify(userObj)));
}

function deleteValue(cell, userId, updatedDateString, dailyUsage) {
  const userObj = {
    userId: userId,
    messages: [],
    updatedDateString: updatedDateString,
    dailyUsage: dailyUsage,
  }
  cell.setValue(JSON.stringify(userObj))
}

function buildMessages(previousContext, userMessage) {
  if (previousContext.length == 0) {
    return [systemRole(), { "role": "user", "content": userMessage }]
  }
  if (previousContext[0]['role'] == 'system') {
    previousContext.shift()
  }
  const messages = [systemRole(), ...previousContext, { "role": "user", "content": userMessage }]
  let tokenNum = 0
  for (let i = 0; i < messages.length; i++) {
    tokenNum += messages[i]['content'].length
  }

  /// メッセージが長すぎる時は削除する
  while (MAX_TOKEN_NUM < tokenNum && 2 < messages.length) {
    tokenNum -= messages[1]['content'].length
    messages.splice(1, 1);
  }
  return messages
}

function callLineApi(replyText, replyToken) {
  try {
    const url = 'https://api.line.me/v2/bot/message/reply';
    UrlFetchApp.fetch(url, {
      'headers': {
        'Content-Type': 'application/json; charset=UTF-8',
        'Authorization': 'Bearer ' + props.getProperty('LINE_ACCESS_TOKEN'),
      },
      'method': 'post',
      'payload': JSON.stringify({
        'replyToken': replyToken,
        'messages': [{
          'type': 'text',
          'text': replyText,
        }]
      })
    })
  } catch (e) {
    debug(e)
  }
}

function doPost(e) {
  const event = JSON.parse(e.postData.contents).events[0]
  const replyToken = event.replyToken
  const userId = event.source.userId
  const nowDate = new Date()

  const cell = getUserCell(userId)
  const value = cell.getValue()
  let previousContext = []
  let userData = null
  let dailyUsage = 0

  if (value) {
    userData = JSON.parse(value)
    const decryptedMessages = []
    for (let i = 0; i < userData.messages.length; i++) {
       decryptedMessages.push({ "role": userData.messages[i]['role'], "content": sc.decrypt(userData.messages[i]['content']).toString() })
    }
    userData.messages = decryptedMessages
    /// UserID があっている場合のみメッセージを取得する
    if (userId == userData.userId) {
      previousContext = userData.messages
      const updatedDate = new Date(userData.updatedDateString)
      dailyUsage = userData.dailyUsage ?? 0
      if (updatedDate && isBeforeYesterday(updatedDate, nowDate)) {
        //使用日が昨日以前の場合初期化
        dailyUsage = 0
      }
    }
  }

  const userMessage = event.message.text
  if (!userMessage) {
    // メッセージ以外(スタンプや画像など)が送られてきた場合
    return
  } else if (userMessage.trim() == "忘れて" || userMessage.trim() == "わすれて") {
    if (userData && userId == userData.userId) {
      /// UserID があっている場合のみ記憶を削除する
      deleteValue(cell, userId, userData.updatedDateString, dailyUsage)
    }
    callLineApi('記憶を消去しました。', replyToken)
    return
  }

  if (MAX_DAILY_USAGE && MAX_DAILY_USAGE <= dailyUsage) {
    callLineApi(countMaxMessage, replyToken)
    return
  }

  const messages = buildMessages(previousContext, userMessage)
  let botReply;
  try {
    botReply = callChatApi(messages)
    if (userData && userId == userData.userId || !value) {
      /// UserID があっているか、初期の場合のみメッセージを保存する
      insertValue(cell, messages, userId, botReply, nowDate, dailyUsage + 1)
    }
  } catch (e) {
    debug(e)
    callLineApi(errorMessage, replyToken)
    return
  }

  callLineApi(botReply, replyToken)
}

function isBeforeYesterday(date, now) {
  const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  return today > date
}

function callChatGPTApi(messages) {
  const requestOptions = {
    "method": "post",
    "headers": {
      "Content-Type": "application/json",
      "Authorization": "Bearer " + props.getProperty('OPENAI_APIKEY')
    },
    "payload": JSON.stringify({
      "model": "gpt-3.5-turbo",
      "messages": messages,
    }),
  }
  const response = UrlFetchApp.fetch("https://api.openai.com/v1/chat/completions", requestOptions)
  const json = JSON.parse(response.getContentText())
  const botReply = json['choices'][0]['message']['content'].trim()
  return botReply
}

function callAzureApi(messages) {
  const requestOptions = {
    "method": "post",
    "headers": {
      "Content-Type": "application/json",
      "api-key": props.getProperty('AZURE_KEY')
    },
    "payload": JSON.stringify({
      "messages": messages,
    }),
  }
  const response = UrlFetchApp.fetch(props.getProperty('AZURE_ENDPOINT'), requestOptions)
  const json = JSON.parse(response.getContentText())
  const botReply = json['choices'][0]['message']['content'].trim()
  return botReply
}

function isAzureAvailable() {
  return props.getProperty('AZURE_KEY') && props.getProperty('AZURE_ENDPOINT')
}

function callChatApi(messages) {
  if (isAzureAvailable()) {
    console.log('using Azure OpenAI Service')
    return callAzureApi(messages)
  }
  return callChatGPTApi(messages)
}

function getAllUserData(inactiveHours = null) {
  const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
  const userDataList = []
  const nowDate = new Date()
  const isInactiveUser = (userData) => {
    if (!inactiveHours || !userData.updatedDateString) {
      return true
    }
    const updatedDate = new Date(userData.updatedDateString)
    const diff = nowDate.getTime() - updatedDate.getTime()
    const diffHour = diff / (1000 * 60 * 60)
    return diffHour > inactiveHours
  }
  for (let s = 1; s < SHEET_NUMBER; s++) {
    const sheet = ss.getSheetByName(s.toString());
    const sheetValues = sheet.getDataRange().getValues().flat()
    for (let i = 0; i < sheetValues.length; i++) {
      if (!sheetValues[i]) {
        continue
      }
      const userData = JSON.parse(sheetValues[i])
      if (userData.userId && isInactiveUser(userData)) {
        userDataList.push(userData)
      }
    }
  }
  return userDataList
}


function sendPushMessagesToAllUsers() {
  sendPushMessages(getAllUserData())
}

function sendPushMessagesToInActiveUsers() {
  const inactiveHours = 30 * 24
  const userDataList = getAllUserData(inactiveHours)
  sendPushMessages(userDataList)
}

function sendPushMessagesToSpecifiedUsers() {
  const userDataList = getAllUserData()
  const specifiedUserIds = ['Uf1819e72137a873aa464063256543f']
  const specifiedUserDataList = userDataList.filter((u) => specifiedUserIds.includes(u.userId))
  sendPushMessages(specifiedUserDataList)
}

function sendPushMessages(userDataList) {
 const pushAssistantPrompt = `
# あなたへの命令:
ユーザーに対して、最近メッセージがなくて寂しい旨を、以前の会話を考慮して一言お願いします。
# 一言:
`
  for (let i = 0; i < userDataList.length; i++) {
    const userData = userDataList[i]
    if (!userData.messages || userData.messages.length <= 1) {
      continue
    }
    if (2 <= userData.messages.length && userData.messages[userData.messages.length - 2]['role'] == 'assistant') {
      continue
    }

    const decryptedMessages = []
    for (let i = 0; i < userData.messages.length; i++) {
       decryptedMessages.push({ "role": userData.messages[i]['role'], "content": sc.decrypt(userData.messages[i]['content']).toString() })
    }
    userData.messages = decryptedMessages
    const previousContext = userData.messages

    const messages = buildMessages(previousContext, '')
    messages.pop()
    messages.push({ 'role': 'assistant', 'content': pushAssistantPrompt })
    console.log(JSON.stringify(messages))
    const botReply = callChatApi(messages)
    UrlFetchApp.fetch("https://api.line.me/v2/bot/message/push", {
      "method": "post",
      "headers": {
        "Content-Type": "application/json",
        'Authorization': 'Bearer ' + props.getProperty('LINE_ACCESS_TOKEN')
      },
      "payload": JSON.stringify({
        "to": userData.userId,
        "messages": [{
          "type": "text", "text": botReply
        }]
      })
    })
    const userCell = getUserCell(userData.userId)
    messages.pop()
    insertValue(userCell, messages, userData.userId, botReply, new Date(userData.updatedDateString ?? new Date()), userData.dailyUsage ?? 0)
  }
}

function outputAllUserDataToSpreadsheet() {
  const data = getAllUserData()
  const ss = SpreadsheetApp.openById(SPREADSHEET_ID)
  const sheetName = 'data'
  let sheet = ss.getSheetByName(sheetName)
  if (sheet) {
    sheet.clear()
  } else {
    ss.insertSheet(sheetName)
    sheet = ss.getSheetByName(sheetName)
  }
  sheet.appendRow(['ユーザー数', data.length])
  data.sort((a, b) => {
    if (a.updatedDateString == null || b.updatedDateString == null) {
      if (a.updatedDateString == b.updatedDateString) return 0;
      if (a.updatedDateString == null) return 1;
      return -1;
    }
    return a.updatedDateString.localeCompare(b.updatedDateString);
  })
  for (const userData of data) {
    if (!userData.messages || userData.messages.length == 0) {
      continue
    }
    sheet.appendRow([userData.userId, userData.updatedDateString, ...userData.messages.filter((m) => m.role != 'system').map((m) => sc.decrypt(m.content)).reverse()])
  }
}

function testCallChatApi() {
  const response = callChatApi([systemRole(), { 'role': 'user', 'content': 'hi' }])
  console.log(response)
}

function testGetScriptProperties() {
  const props = PropertiesService.getScriptProperties()
  console.log(props.getProperty('OPENAI_APIKEY').substring(0, 20))
  console.log(props.getProperty('LINE_ACCESS_TOKEN').substring(0, 20))
  console.log(props.getProperty('SPREADSHEET_ID').substring(0, 20))
  console.log(parseInt(props.getProperty('MAX_DAILY_USAGE')))
}

function testIsBeforeYesterday() {
  console.log('testIsBeforeYesterday')
  const day1 = new Date('2023-03-24')
  const day2 = new Date('2033-03-24')
  console.log(isBeforeYesterday(day1, new Date()))
  console.log(isBeforeYesterday(day2, new Date()))

  console.log(day1.toISOString())
  console.log(new Date(day1.toISOString()))
  console.log((new Date(day1.toISOString())).toISOString())
  console.log(day1.toISOString() == (new Date(day1.toISOString())).toISOString())
}

function testDebug() {
  debug('test debug function')
}

Step3.暗号化ライブラリをインストールする

上記のソースコードの中で、ユーザーメッセージの暗号化および複合化を行なっている都合上、ライブラリのインストールが必要になります。
まず、GAS の左のメニューのライブラリのプラスボタンをクリックします。


スクリプトIDの部分に以下のIDを貼り付けます:

1-QSJlFRNb-6MhZe4zra6tbTTriX_IHbZ7X3nNoFfKtlkA3DrbY-Z-S4i

検索で出てくるライブラリを確認して、追加を押します。

Step4.  各種スクリプトプロパティを設定する

無料版では OPENAI_APIKEY と LINE_ACCESS_TOKEN のみスクリプトプロパティに設定していましたが、ここでは、 SPREADSHEET_ID, OPENAI_APIKEY, LINE_ACCESS_TOKEN , MAX_DAILY_USAGE, の4つのパラメータをスクリプトプロパティで管理します。MAX_DAILY_USAGEについてはユーザー毎の1日毎の使用回数制限ですが、もし制限が必要ない場合には、このパラメータは設定する必要はありません。
まず、一番左の設定アイコンをクリックして、プロジェクトの設定画面に移動します。

この画面の一番下に、スクリプトプロパティを追加というボタンがあるため、ここをクリックして、各種パラメータを設定します。

以下のように、4つのパラメータを設定します。これらのパラメータの詳細については、上記ですでに説明しているため割愛させていただきます。

  • SPREADSHEET_ID

  • OPENAI_APIKEY

  • LINE_ACCESS_TOKEN

  • MAX_DAILY_USAGE

MAX_DAILY_USAGEについてはユーザー毎の1日毎の使用回数制限ですが、もしユーザー毎の制限が必要ない場合には、このパラメータを設定する必要はありません。なお、これらのパラメータを変更した時に再度デプロイする必要はありません。必要に応じて、MAX_TOKEN_NUMやsystemPromptをこのようにパラメータ化することもできるため、必要であればお試しください。

Step2のコードを保存することで、以下の画像のように、上に関数のメニューが出てきます。必要に応じて、こちらのメニューで、testGetScriptProperties を選択して、実行をクリックすることにより、上記のパラメータが正しく設定されているかをテストすることができます。

実行ログが以下のようになっていれば成功です。

Step5.シートを50個作成する

続いて、ユーザーデータの保存データを分散させるために、スプレッドシートのタブを 50 個作成します。これについては、関数を用意しているため、そちらを実行するだけです。
上記のコードを保存することで、以下の画像のように、上に関数のメニューが出てきます。こちらのメニューで、createSheets を選択して、実行をクリックします。

これにより、1 ~ 50 番のシートが、対象の Spreadsheet に作られます。
Step1で作成したSpreadsheet に戻り、正しく作成されていることを確認します。

こちらで50個作っている理由は、ユーザーIDで制御してユーザーデータをそれぞれのシートに分散している設計になっているからです。Spreadsheetで扱える限界のセル数、約1000万セルにユーザーIDをハッシュ関数で分散しています。そのため、ユーザーデータが挿入されると、シートの特定の領域にマッピングされます。万が一、セルが衝突した場合は衝突後のユーザーは文脈を保持できない設計になっております。ほとんど起こらないとは思いますが、まれに起こる可能性はあります。この点については申し訳ありませんが、ご留意ください。将来的に修正する可能性はあります。

これで、追加の手順は完了です。あとは無料部分で説明したように、この関数をデプロイし、得られたURLをWebhook URLに入れれば全て完了です。
動作を確認してみましょう。

デバッグ方法

最後にデバッグ方法を紹介します。
debug 関数をGAS内に用意しているため、デバッグしたい時に呼び出すことで、中の変数が、logs シートに書き込まれます。
既に、API などの部分では エラーが出た時に debug 関数が呼ばれる仕組みにしてあるため、うまく動作しないなどの時は、必要に応じて Spreadsheet の logs シートを 確認してください。

3/14更新:エラーメッセージの送信

エラーが出た時には、ユーザーにエラーメッセージを送信するようにソースコードを変更しました。必要に応じてエラーメッセージを修正して、上記のソースコードにアップデートをお願いします。

3/25更新:ユーザー毎に1日の使用回数の制限を行えるように

使用回数を制限したい場合には、上記のSTEP3を参考にMAX_DAILY_USAGEの値を設定することで、使用回数の制限が行えます。また countMaxMessage のテキストを変更することで、使用回数を超過した場合のメッセージを設定できます。デフォルトは、「1日の最大使用回数${MAX_DAILY_USAGE}回を超過しました。」としています。

3/27更新:スクリプトプロパティの利用

OpenAI API key などのセキュリティキーおよびユーザー毎に1日の使用回数の制限について、スクリプトプロパティを使用するように変更しましたので、必要に応じて、上記の STEP3 を参考にスクリプトプロパティを使用することで、よりセキュリティキーの取り扱いについて利便性が上がります。

5/25更新:Azure OpenAI Service 対応

AZURE_KEYAZURE_ENDPOINT をスクリプトプロパティに入力することで、Azure OpenAI Service に自動で切り替わるようにソースコードを変更しました。AZURE_KEY および、AZURE_ENDPOINT の入手方法については、こちらの記事などをご参照ください。以下のような状態になっていれば、OKです。Azure を使用する場合 OPENAI_APIKEY の設定は必要ありません。

関数のメニューから、testCallChatApi を呼び出すことで、Azure OpenAI Service が正しく使われているかどうかを確認することができます。

5/25更新:パーソナライズされたプッシュ通知について

以下の三つの関数が追加されました。関数を実行することで、それぞれ説明しているユーザーに対してChatGPT を活用したプッシュ通知が行えます。

  • sendPushMessagesToAllUsers

    • 全てのユーザーに通知を送信します。

  • sendPushMessagesToInActiveUsers

    • inactiveHours 時間以上メッセージがないユーザーに通知を送信します。例えば、inactiveHoursを48に設定すれば、二日間メッセージがないユーザーに通知が送られます。

  • sendPushMessagesToSpecifiedUsers

    • 配列の「specifiedUserIds」に指定されたユーザーIDのリストに通知が送られます。テストなどに使えます。ユーザーIDは、後述のデータ分析機能で確認可能です。

これらのプッシュ通知で使われるプロンプトは、 sendPushMessages  内の、pushAssistantPrompt という変数名で指定されています。
この値がプッシュ通知のメッセージのためのプロンプトのため、ここをお好きなように変更してからメッセージを送るようにしてください

pushAssistantPrompt = `
# あなたへの命令:
ユーザーに対して、最近メッセージがなくて寂しい旨を、以前の会話を考慮して一言お願いします。
# 一言:
`

これらの実行はすべて一度に行われるため、エラーが起こる可能性がある点にはご留意ください。また、プッシュ通知は、Line のメッセージ送信機能に該当するため、その点もご注意ください。
プッシュ通知を送った後にユーザーが反応していないのに、プッシュ通知を再度送ろうとしても送らないようになっています。そのため、GoogleAppsScript のトリガー機能などで、sendPushMessagesToInActiveUsers を定期実行するなどの運用も可能です。

5/25更新:データ分析機能について

outputAllUserDataToSpreadsheet 関数を作成しました。この関数を実行すると、スプレッドシートに data シートが作成され、そこにユーザー(匿名)とボットの会話データが出力されます。こちらの機能はあくまでボットの改善目的に使い、サービスの運営者として慎重に使われることをお勧めします

Line botが動かないときのチェックリスト

  1. Lineアクセストークンを生成する際、ブラウザの翻訳機能やLine Developerの自動翻訳がオンになっていないか確認してください。間違っていると401エラーが発生します。

  2. スプレッドシートIDの貼り付け範囲に注意してください。間違っているとスプレッドシートのシート生成に失敗します。

  3. OpenAIのAPIキーの貼り付け時に、キーの端にスペース文字が含まれていないか確認してください。

  4. スクリプトを修正する際に、「`」の記号を「'」に変更していないか確認してください。間違っているとデプロイが失敗します。

  5. アクセスできるユーザーを「全員」に設定し、「Googleアカウントを持つ全員」を選択していないか確認してください。間違っていると、botが無応答になりエラーも表示されません。

  6. スクリプトを変更したら必ずデプロイし、生成されるURLをLine Developerの管理画面に貼り付けてください。スクリプトを保存しただけではデプロイされません。

  7. Line Developerのチャット機能がオフで、Webhookがオンになっていることを確認してください。間違っていると、Lineのチャット内で既読になりません。

  8. Googleスプレッドシートに50シートあることを確認してください。シートが正しくないと、チャット内で「既読」となっても応答を返しません。

  9. OpenAIの管理画面で残高がなくなっていないか確認してください。無料利用分が付与されていない場合は、クレジット登録が必要です。

  10. OpenAIのAPIとPlusの利用枠は別であることを認識しておいてください。GAS(Google Apps Script)は無料利用の範囲内です。

  11. 上記を確認しても解決に至らない場合には、以下のDiscordコミュニティに入り、助け合いチャンネルで、過去の会話を検索するか、投稿すれば、誰かが答えてくれます。大抵はここで解決できます。質問する際は、「どこで」「何をしたら」「どこに」「何が表示された」を具体的に含めてください。画面キャプチャがあるとわかりやすいですが、画面内にキー情報が含まれている場合は塗りつぶして投稿してください。

  12. また、中級者向けですが、会話中にBotの喋り方が変わってしまうのを止めたい場合なども、コミュニティ内で議論されています。

最後に

もし、面白いLinebot を作ることに成功したら、他の人にもぜひ共有したいため、Twitter でメンションをつけて共有して頂けると大変嬉しいです!
みんなでいろんな Linebot を量産しましょう!

記事を購入してくださった方限定にお知らせ

さらに、この記事を購入してくださった方限定で、特別なお知らせがあります。 私たちは新しくDiscordコミュニティーを作りました🎉  ここでは、LINEbotやChatGPTについてコミュニティメンバー同士で交流したり、バグ報告したり、AIの進化について共有しあったりできる場所です。また、Discordメンバーしか見れない、ChatGPT研究所の最新の研究も配信していく予定です。AIをより良い方向に発展させるためには、皆さんのご意見やご感想がとても大切です。 ぜひ参加してくださいね! Discordコミュニティーへの招待リンクはこちらです👉

https://discord.gg/PQg9C2JNEj

お待ちしています!