💡

文脈対応のWebChatGPT風LINE Botを作る

2023/04/04に公開

1. LINE Botの作成

Google Apps Scriptでオウム返しBotを作成

Google Apps Script(以下、GAS)にアクセスして新しいプロジェクトを作成し、
コードエディタを開いて以下のコードをコピペ。
https://script.google.com/home
このコードはChatGPTに「GASでオウム返しをするLINE botを作って」と言ったら
数秒で作ってくれました。

function doPost(e) {
  const CHANNEL_ACCESS_TOKEN = 'LINEのアクセストークンを入力';
  let json = JSON.parse(e.postData.contents);
  let replyToken = json.events[0].replyToken;
  let message = json.events[0].message.text;
  
  if (typeof replyToken === 'undefined') {
    return;
  }
  
  let url = 'https://api.line.me/v2/bot/message/reply';
  let headers = {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN,
  };
  let data = {
    'replyToken': replyToken,
    'messages': [
      {
        'type': 'text',
        'text': message,
      },
    ],
  };
  let options = {
    'method': 'post',
    'headers': headers,
    'payload': JSON.stringify(data),
  };
  
  UrlFetchApp.fetch(url, options);
}

ユーザーの発言がmessageに入り、dataオブジェクトのmessagestextキーの中身が返事です。
今回はmessageをそのままtextに入れているので、ユーザーの発言内容をそのまま返します。

LINEチャネルを作成してアクセストークンを取得

新しいタブでLINE Developersにアクセスして右上のログインを押してログイン。
https://developers.line.biz/ja/
LINEアカウントでもビジネスアカウントでもどちらでも構いません。
とりあえずはLINEアカウントから始めるのがいいのではないでしょうか。

プロバイダーの画面で作成を押して」名前を入力してプロバイダーを作成。
チャネル設定画面でMessaging APIを選択して必要な項目を入力して作成を押す。
チャネルが作成されるのでクリックしてMessaging API設定から一番下に移動して、
チャネルアクセストークン(長期)で発行を押すとトークンが発行されます。
トークンをコピーしたらGASに戻り、コードにトークンを入力します。
''で囲むのを忘れないように。

デプロイをしてWebhook URLを設定

GASの右上のデプロイから新しいデプロイを選択してデプロイ。
するとURLが発行されるのでコピーしてLINE Developersのタブに移動。
Messaging API設定のWebhook URLにGASのURLを設定。
念のため検証を押して成功となることを確認しましょう。

LINEで友だち登録をして動作確認

Messaging API設定のボット情報でIDかQRコードを使って友だち登録をしたら
LINEで何かメッセージを送ってみて返答が返ってくることを確認しましょう。
これでオウム返しBotは完成です。

2. LINE Bot with ChatGPTの作成

ここからはこの返事をChatGPTに応えてもらうように作っていきます。

GASのコードにChatGPTとの応答を追加

以下のコードを追加して、messageを渡すとChatGPTの返答を返す関数を作成。

function getChatGPTResponse(message) {
 const API_KEY = 'OpenAIのAPIキーを入力'
  const url = 'https://api.openai.com/v1/chat/completions';
  let headers = {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer ' + API_KEY,
  };
  let context = []
  context.push({ role: "user", content: message });
  let data = {
    'model': 'gpt-3.5-turbo',
    'messages': context,
    'max_tokens': 300,
    'temperature': 0.5
  };
  let options = {
    'method': 'post',
    'headers': headers,
    'payload': JSON.stringify(data)
  };
  let response = UrlFetchApp.fetch(url, options);
  let json = JSON.parse(response.getContentText());
  let choices = json.choices;
  let text = choices[0].message.content.replace(/^\s+|\s+$/g, '');
  return text;
}

getChatGPTResponseができたら、先程入力したdoPost関数を編集。
messageの部分を、getChatGPTResponse(message)に変更します。

  var data = {
    'replyToken': replyToken,
    'messages': [
      {
        'type': 'text',
+       'text': getChatGPTResponse(message),
-       'text': message,
      },
    ],
  };

OpenAI APIのAPIキーを取得

新しいタブでOpenAIのProductのOverviewの画面を開いたら
[Get started↗]というボタンを押してログイン。
アカウントがなければ新規で作成してログインしてください。
https://openai.com/product
右上のPersonalと書いてあるところをクリックしてView API Keysを選択し
[+Create new secret key]をクリックしてAPIキーを取得します。

APIキーをコピーしたらGASのタブに戻ってAPIキーをペーストしてください。
このAPIキーは表示を消してしまうと二度と確認できないので注意。
といっても新たに発行すればいいのですが。

APIキーを設定できたら、ChatGPTのLINE Botが完成です。
LINEからメッセージを送って返事を確認しましょう。
オウム返しBotの時よりも返答に30秒~1分ほど時間がかかります。

3. WebChatGPT風LINE Botを作成

ChatGPTは非常に流暢な文章を返してくれますが
その内容は2021年時点までのデータを元にしているため
新しいことなどについては誤った情報を返します。
そのため入力内容をウェブ検索して新しい情報を盛り込んだ上で
回答内容を作成してもらうようにしましょう。

Bing AIがまさにそのような仕組みをしているのですが、
オフィシャルのChatGPTでもそのような検索をしてくれる
Chrome拡張機能がWebChatGPTです。
https://chrome.google.com/webstore/detail/webchatgpt-chatgpt-with-i/lpfemeioodjbpieminkklglpmhlngfcn?hl=ja

それと同様な機能をこのLINE Botにも組み込んでみましょう。

発言内容を検索するコードを追加

GASのエディタを開いて以下のコードを追加。
Googleで入力内容を検索して1件目のURLを取得し、
そのURLのサイト内容の最初の2,000字を返す関数です。

function getResultContent(value) {
  let firstResultContent = '';
  let firstResultUrl = '';
  let searchResults = googleSearch(value);
 firstResultUrl = searchResults.items[0].link;
  if (firstResultUrl) {
    firstResultContent = fetchWebpageContent(firstResultUrl);
  }
  return firstResultContent;
}

function googleSearch(query) {
  const apiKey = 'Google Cloud PlatformのAPIキーを入力';
  const searchEngineId = '検索エンジンIDを入力';
  const url = `https://www.googleapis.com/customsearch/v1?key=${apiKey}&cx=${searchEngineId}&q=${query}`;
  const options = {
    'method': 'get'
  };
  const response = UrlFetchApp.fetch(url, options);
  const data = JSON.parse(response.getContentText());
  return data;
}

function fetchWebpageContent(url) {
  const response = UrlFetchApp.fetch(url);
  const html = response.getContentText();
  let content = parseHtml(html);
  // 指定された文字数以上を切り捨て
  if (content.length > 2000) {
    content = content.substring(0, 2000);
  }
  return content;
}

さてgetChatGPTResponse関数を編集してこれをChatGPTに渡しましょう。

+ let resultContent = getResultContent(message);
+ resultContent = '以下の情報を前提として今後の質問に回答して。\n' + resultContent;
  let context = []
+ context.push({ role: "system", content: resultContent })
  context.push({ role: "user", content: message });

これで検索内容を前提とした回答を作成してもらうことができます。
あとはAPIキーと検索エンジンIDを設定しましょう。

Google CloudのAPIキーを取得

新しいタブでloud Platformにアクセスして左上のメニューから「APIとサービス」を選び
その中の「認証情報」のページを開きます。
https://console.cloud.google.com/?hl=ja
[+認証情報を作成]からAPIキーを指定してAPIキーをコピーします。
GASのタブに戻ってAPIキーを入力します。

メニューから「有効なAPIとサービス」を選択し[+APIとサービスの有効化]から
Custom Search APIを検索して有効化します。

検索エンジンIDを取得

新しいタブで検索エンジンにアクセスして「追加」をクリック。
https://programmablesearchengine.google.com/controlpanel/all
必要な設定をしたら作成を押して検索エンジンを作成します。
概要の中の基本に検索エンジンIDが載っているのでコピーします。
GASのタブに戻って検索エンジンIDを入力します。
これでWebChatGPT風LINE Botの完成です。

4. 文脈に対応したLINE Botの作成

ここまでで単発の会話は問題なくできるようになりました。
しかしChatGPTの回答は毎回新規の発言内容で作成されているため
過去の発言の流れからの文脈に沿った会話をすることができません。
回答が途中で途切れてしまっても続きを書いてもらうことができず、
以前の内容を反映した質問をしても答えてもらえません。
そこで過去の会話を保存する仕組みを作ります。

FirebaseAppの追加

GASエディタのライブラリの右の+を押して以下のスクリプトIDを入力。
'1VUSl4b1r1eoNcRWotZM3e87ygkxvXltOgyDZhixqncz9lQ3MjfT1iKFw'
https://github.com/grahamearley/FirestoreGoogleAppsScript
(ここのライブラリですが、このページにアクセスする必要はありません。)

Firestoreに保存するコードを追加

Firestoreから今までの会話を取得&保存&削除するコードを追加します。
まずFirestoreの保存場所を決めるためにdoPost関数を編集してUserIDを取得します。

  let json = JSON.parse(e.postData.contents);
  let replyToken = json.events[0].replyToken;
  let message = json.events[0].message.text;
+ let uid = event.source.userId;

またgetChatGPTResponseを呼び出す部分もUserIDを使うように編集します。

  var data = {
    'replyToken': replyToken,
    'messages': [
      {
        'type': 'text',
+       'text': getChatGPTResponse(uid, userMessage)
-       'text': getChatGPTResponse(message),
      },
    ],
  };

doPost関数の編集が終わったら、以下のコードを追加してください。
ChatGPTのトークン数は4096が上限なのでそれを超えないように、
取得時に3000字を超えたらそれより古い会話は取得せずにFirestoreからも削除します。

const dateArray = {
    "email": "Firebaseサービスアカウントを入力",
    "key": "秘密鍵を入力",
    "projectId": "プロジェクトIDを入力"
};

function putContext(uid, key, role, value){
  let firestore = FirestoreApp.getFirestore(dateArray.email, dateArray.key, dateArray.projectId);
  let data = { [role] : value };
  firestore.updateDocument("users/" + uid + "/chatgpt/" + key, data, true);
}

function getContext(uid){
  let firestore = FirestoreApp.getFirestore(dateArray.email, dateArray.key, dateArray.projectId);
  let chat_doc = firestore.getDocuments("users/" + uid + "/chatgpt/");
  let chats = [];
  let chat_contents = message + searchResult;
  let context = '';
  let deletion = [];
  for (let i = chat_doc.length; i > 0; i--) {
    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[chatRole];
    chat_contents += chatContent;
    if (chat_contents.length > 2500) {
      let chatLines = chatContent.split('\n');
      firestore.deleteDocument("users/" + uid + "/chatgpt/" + chatDate);
    } else {
      chats.push({ date: chatDate, role: chatRole, content: chatContent })
    }
  }
  let after_chat_doc = firestore.getDocuments("users/" + uid + "/chatgpt/");
  let sortedchats = chats.sort((a, b) => a.date - b.date);
  context = sortedchats.map(({date, ...rest}) => rest);
  return context;
}

function deleteContext(uid) {
  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);
  });
}

会話内容を取得&保存するためにgetChatGPTResponse関数も編集します。

-function getChatGPTResponse(message) {
+function getChatGPTResponse(uid, message) {
 const API_KEY = 'OpenAIのAPIキーを入力'
  const url = 'https://api.openai.com/v1/chat/completions';
  let headers = {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer ' + API_KEY,
  };
  let resultContent = getResultContent(message);
  resultContent = '以下の情報を前提として今後の質問に回答して。\n' + resultContent;
- let context = [];
+ let context = getContext(uid);
  context.push({ role: "system", content: resultContent })
  context.push({ role: "user", content: message });
    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, '');
    if (resultContent) putContext(uid, Date.now(), 'system', resultContent);
    if (message.match('続き') || message.match('continue')) putContext(uid, Date.now(), 'user', message);
    putContext(uid, Date.now(), 'assistant', text);
    if (choices[0].finish_reason !== 'stop') {
      text += '\n(文章が途切れていたら「続き」と入力してください。)';
    }

  let response = UrlFetchApp.fetch(url, options);
  let json = JSON.parse(response.getContentText());
  let choices = json.choices;
  let text = choices[0].message.content.replace(/^\s+|\s+$/g, '');
+ putContext(uid, Date.now(), 'system', resultContent);
+ if (message.match('続き') || message.match('continue')) {
+   putContext(uid, Date.now(), 'user', message);
+ }
+ putContext(uid, Date.now(), 'assistant', text);
return text;

ではFirestore Databaseに必要な情報を取得しましょう。

Firestore Databaseの作成

Firestoreにアクセスしてコンソールへ移動。
https://firebase.google.com/docs/firestore?hl=ja
プロジェクトを追加して必要な情報を入力したらプロジェクトを作成。

構築からFirestore Databaseを選んでデータベースを作成。
テストモードで開始するを選んでロケーションを選択して作成します。

各種情報の取得

プロジェクトの横の概要横の歯車からプロジェクトの設定を選び
全般タブでプロジェクトIDを確認してGASに入力します。
サービスアカウントでFirebaseサービスアカウントを確認してGASに入力、
さらに同じページで新しい秘密鍵を作成をクリックしてGASに入力。

これで文脈に対応したLINE Botが完成しました。

最後に

いろいろとエラー処理や例外処理を省いており、
セキュリティ的にも問題のあるコードになっています。
お試し程度に使うにとどめてください。

Discussion