👏

GPT-4oとClaude3 Opusに対応したLINE Chatbotを作成する

2024/03/14に公開

0. 何を作るのか

LINEでメッセージを送信するとChatGPTやClaudeが回答してくれるようなChatbotを作ります。一回きりのやり取りではなく、発言内容を記憶して会話ができるようなものにしましょう。

1. LINEとGoogle Apps Script(GAS)の接続

1-1. LINEチャネルの開設

LINE Developpers Consoleでチャネル設定からMessaging APIを選択して、必要項目を入力してチャネルを作成。

1-2. GASプロジェクトの作成

LINE Developpers Consoleとは別のタブでGoogle Apps Scriptを開いて新しいプロジェクトを作成。
コード.gsに以下のコードを入力。

// LINE developersのMessaging API設定のチャネルアクセストークン
const LINE_TOKEN = '(取得したチャネルアクセストークン(長期)を入力)';
const LINE_URL = 'https://api.line.me/v2/bot/message/reply';

//postリクエストを受取ったときに発火する関数
function doPost(e) {
  let event = JSON.parse(e.postData.contents).events[0];
  let replyToken = event.replyToken;
  let lineType = event.type;
  const uid = event.source.userId;

  if (typeof replyToken === undefined || lineType === 'follow' || lineType === 'unfollow') {
    return;
  }

  let messages = [];  // 送信するメッセージを初期化

  // ユーザーのメッセージを取得
  let userMessage = event.message.text;
  let spl_msg;
  if(userMessage) spl_msg = userMessage.split("\n");

  // キャッシュサービスのロード
  const cache = CacheService.getScriptCache();
  let modelFromCache = cache.get(uid + "_model");
  let model = (modelFromCache !== null) ? modelFromCache : "gpt-4-turbo";
  
  if (!userMessage) {
    // 入力がなければ何もしない
  } else if (spl_msg[0] === '忘れて' || spl_msg[0] === 'リセット') {
    messages.push({ type: "text", text: '会話の内容を削除しました。' });
    deleteContext(uid);

  } else if (spl_msg[0] === 'gpt-4o') {
    messages.push({ type: "text", text: 'モデルをGPT-4oに設定しました。この設定は6時間有効です。' });
    cache.put(uid + '_model', 'gpt-4o', 21600);
  } else if (spl_msg[0] === 'claude' || spl_msg[0] === 'opus') {
    messages.push({ type: "text", text: 'モデルをclaude-3-opus-20240229に設定しました。この設定は6時間有効です。' });
    cache.put(uid + '_model', 'claude-3-opus-20240229', 21600);
  } else if (spl_msg[0] === 'both') {
    messages.push({ type: "text", text: 'モデルをGPT-4oとclaude-3-opus-20240229に設定しました。この設定は6時間有効です。' });
    cache.put(uid + '_model', 'both', 21600);

  } else {
    if (model == 'both') {
      messages.push({ type: "text", text: getChatGPTResponse(uid, userMessage, 'gpt-4o') });
      messages.push({ type: "text", text: getClaudeResponse(uid, userMessage, 'claude-3-opus-20240229') });
    } else if (model.includes('claude')) {
      messages.push({ type: "text", text: getClaudeResponse(uid, userMessage, model) });
    } else {
      messages.push({ type: "text", text: getChatGPTResponse(uid, userMessage, model) });       
    }
  }

  UrlFetchApp.fetch(LINE_URL, {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': `Bearer ${LINE_TOKEN}`,
    },
    'method': 'post',
    'payload': JSON.stringify({
      'replyToken': replyToken,
      'messages': messages,
    }),
  });
  ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON);
}

とりあえずチャネルアクセストークンは設定せずにウェブアプリとしてデプロイし、ウェブアプリのURLをコピーする(末尾がexec)。

1-3. LINEとGASをWebHookで接続

LINE Developpers Consoleのタブに戻る。
Messaging API設定のタブにある、Webhook URLに先ほどコピーしたGASのウェブアプリのURLを設定し検証が通ることを確認し、Webhookの利用をオンにする。

Messaging API設定ページ末尾でチャネルアクセストークンを取得してコピーする。
GASのタブに戻ってコードに入力する。(セキュリティ的には直書きでなくGASのスクリプトプロパティを使うほうがよい。)

これでこのLINEチャネルに投稿されたメッセージがGASに届くようになりました。

2. ChatGPTの設定

次にgetChatGPTResponse関数を実装します。

function getChatGPTResponse(uid, message, model) {
  let url = 'https://api.openai.com/v1/chat/completions';
  let token = '(ChatGPT APIのSecret Key)';
  let headers = {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer ' + token
  };
  let max_length = 120000;
  let [context, deletion] = getContext(uid, message, max_length);
  let max_tokens = 4096;
  let payload = {
    'model': model,
    'messages': context,
    'max_tokens': max_tokens,
    'temperature': 0.2
  };
  let options = {
    'method': 'post',
    'headers': headers,
    'payload': JSON.stringify(payload)
  };
  let text = '';
  try {
    let response = UrlFetchApp.fetch(url, options);
    let jsonResponse = JSON.parse(response.getContentText());
    let choices = jsonResponse.choices;
    text = choices[0].message.content.replace(/^\s+|\s+$/g, '');
    putContext(uid, Date.now(), 'user', message);
    putContext(uid, Date.now(), 'assistant', text);
    if (choices[0].finish_reason !== 'stop') {
      text += '\n(文章が途切れていたら「続き」と入力してください。)';
    }
  } catch (error) {
    text = 'ChatGPTへの問い合わせ中にエラーが発生しました' +  error.message;
  }
  return text;
}

Open AIのAPI KeysでChatGPTのAPIのsecret keyを作成してコピーし、上記のコードに入力する。

3. Claudeの設定

次にgetClaudeResponse関数を実装します。

function getClaudeResponse(uid, message, model) {
  let url = 'https://api.anthropic.com/v1/messages';
  let token = '(Anthropic APIのKey)';
  let headers = {
    'x-api-key': token,
    'anthropic-version': '2023-06-01',
    'Content-Type': 'application/json'
  };
  let max_length = 120000;
  let [context, deletion] = getContext(uid, message, max_length);
  if (context[0].role !== 'user') {  // claudeは先頭がuserでないとエラーが出る
    context.shift();
  }
  let max_tokens = 4096;
  let payload = {
    'model': model,
    "max_tokens": max_tokens,
    'temperature': 0.2,
    'messages': context
  };
  let options = {
    'method' : 'post',
    'headers': headers,
    'payload' : JSON.stringify(payload)
  };

  let text = '';
  try {
    let response = UrlFetchApp.fetch(url, options);
    let jsonResponse = JSON.parse(response.getContentText());
    let content = jsonResponse.content;
    text = content[0].text.replace(/^\s+|\s+$/g, '');
    putContext(uid, Date.now(), 'user', message);
    putContext(uid, Date.now(), 'assistant', text);
    if (jsonResponse.stop_reason == 'max_tokens') {
      text += '\n(文章が途切れていたら「続き」と入力してください。)';
    }
  } catch (error) {
    text = 'Claudeへの問い合わせ中にエラーが発生しました' +  error.message;
  }
  return text;
}

AnthropicのAPI KeysでKeyを作成してコピーし、上記のコードに入力する。

4. FireStoreの設定

このままでも1回きりに応答はできるのですが、発言した内容を記憶して続けて会話ができるようにしましょう。
記憶する場所としてはGoogleのFireStoreを使用します。

GASに以下のコードを追加します。

// Firestoreに今までの会話を記録する
function putContext(uid, key, role, value){
  let dateArray = firestoreDate();
  let firestore = FirestoreApp.getFirestore(dateArray.email, dateArray.key, dateArray.projectId);
  let data = {
    [role] : value
  }
  firestore.updateDocument("users/" + uid + "/chatgpt/" + key, data, true);
}

// Firestoreから今までの会話を取得する。取得時に指定文字数を超えていたらそれより古い会話は取得せずにFirestoreからも削除する
function getContext(uid, message, max_length){
  let dateArray = firestoreDate();
  let firestore = FirestoreApp.getFirestore(dateArray.email, dateArray.key, dateArray.projectId);
  let chat_doc = firestore.getDocuments("users/" + uid + "/chatgpt/");
  let chats = [];
  let chat_contents = message;  // 全ての応答を結合して一つの文章にした文字列。文字数計数用。
  let context = [];
  let deletion = [];  // 削除した会話の配列
  for (let i = chat_doc.length; i > 0; i--) {  // 新しい順に会話を追加して上限を超えたらそれより古い会話は追加せずDBからも削除する
    let chat = chat_doc[i - 1];
    let chatPath = chat.path.split('/');
    let chatDate = chatPath[chatPath.length - 1];
    let chatRole = Object.keys(chat.obj)[0];
    let chatContent = chat.obj[Object.keys(chat.obj)[0]];
    chat_contents += chatContent;
    if (chat_contents.length > max_length) {
      if (chat_contents.length > 128000) {
        let chatLines = chatContent.split('\n');
        deletion.push(chatLines[0].slice(0,3));  // 削除した会話の一行目を取得してdeletionに追加
        firestore.deleteDocument("users/" + uid + "/chatgpt/" + chatDate);
      }
    } else {
      chats.push({ date: chatDate, role: chatRole, content: chatContent })
    }
  }
  let sortedchats = chats.sort((a, b) => a.date - b.date);  // 日時で順番に並び替える
  context = sortedchats.map(({date, ...rest}) => rest);  // 日時を取り除く
  context.push({ role: "user", content: message });
  return [context, deletion];
}

function deleteContext(uid) {
  let dateArray = firestoreDate();
  let firestore = FirestoreApp.getFirestore(dateArray.email, dateArray.key, dateArray.projectId);
  let collectionName = "users/" + uid + "/chatgpt";
  firestore.getDocuments(collectionName).forEach((document) => {
    let documentPath = document.path.split('/');
    let documentName = documentPath[documentPath.length - 1];
    firestore.deleteDocument(collectionName + "/" + documentName);
  });
}

function firestoreDate() {
  const dateArray = {
    "email": "(秘密鍵ファイルの中のemailを入力)",
    "key": "(秘密鍵ファイルの中のkeyを入力)",
    "projectId": "(秘密鍵ファイルの中のprojectIdを入力)"
  }
  return dateArray;
}

Firebase Consoleでプロジェクトを追加。
作成したプロジェクトの、プロジェクトの設定>サービスアカウントの画面で新しい秘密鍵を生成で認証情報のファイルを得る。
このファイルの内容を下に、firestoreDate関数内のdateArrayに必要な情報を転記する。

6. デプロイ

すべての設定が終わったら、GASのデプロイを管理から新しいバージョンとしてデプロイしましょう。

7. 使い方

メッセージを送信するとデフォルトではGPT-4 Turbo(gpt-4-turbo-preview)が答えてくれます。
回答してくれるモデルを変更する際は、

claude

と送信するとclaude-3-opus-20240229に切り替わります。
さらに

both

と送信すると、GTP-4とClaude3の両方で回答をしてくれます。料金が倍以上かかりますね。
比較ができるので面白いですが、単に連続で呼び出しているだけなので後の方の回答は先の回答の内容も含んだプロンプトになっていることはご留意ください。
記録した会話を忘れさせるには、

忘れて

もしくは

リセット

と送信すれば会話内容をすべて削除します。特定の会話のみを削除することはできません。

Discussion