見出し画像

Discordでの情報収集環境を整える(arXiv編)

情報収集をうまくやりたい!

生成AIを学んでいると直面する問題。それな、とにかくとんでもなく情報が多い!そして流れるのがとんでもなく早い!という問題です。
生成AIは技術の進歩が著しく早く、それこそ一日単位で状況が変わることがざらにあります。
筆者はXで情報を収集していますがどうしても流れてしまい、キャッチアップが上手くできていないのが実情です。
そんな中、IizukaさんのポストでDiscordをプラットフォームとした情報収集術が公開(しかも無料!)されていましたので今回はこちらの中でarXivの最新論文の取得の実装をしてみます。
Iizukaさん、ありがとうございます!

Discordをプラットフォームとした情報収集の足掛かりとして、今回はDiscordに論文の翻訳・要約が自動的に投稿できるように進めていきます。
最終的には以下の画像のようにDiscordに投稿できるようになります。

Discordに論文の翻訳・要約が投稿されるようになります。

arXivの論文翻訳&要約をDiscordに自動的に投稿できるようにしてみよう!

この仕組みの実装はGAS(GoogleAPPScript)とDiscordのWebhook機能によって実現します。
本投稿はIizukaさんが公開してくださっている記事をベースに作成しています。

Discordのwebhookを取得しよう!

まずはDiscordで投稿先のチャンネルのwebhookを取得(作成)します。
尚、本記事ではDiscordのアカウント登録や自サーバの作成、投稿先のチャンネルの作成については割愛します。

Discordのアプリの自サーバから「サーバの設定」から連携サービスを選択します。
「新しいウェブフック」を選択しウェブフックを作成します。
ウェブフックの名前や投稿先のチャンネルを選択して、「ウェブフックURLをコピー」を選択して作成したURLをコピーしてください。

作成されたウェブフック

GASでarXivの論文を取得しよう!

次にGASでコードを作成します。

GASには以下のコードを記述します。コード自体はIizukaさんのコードをそのまま使わせていただいており、コメントだけ追記しております。

 // OpenAI の API keys (https://platform.openai.com/account/api-keys)
 // スクリプトプロパティにAPIキーを保存するには、スクリプトエディタで「ファイル」>「プロジェクトのプロパティ」>「スクリプトプロパティ」を選択し、OPENAI_API_KEYとしてAPIキーを追加してください。
 
 const OPENAI_API_KEY = PropertiesService.getScriptProperties().getProperty("OPENAI_API_KEY");  // OpenAI APIキーをスクリプトプロパティから取得
 const OPENAI_API_ENDPOINT = "https://api.openai.com/v1/chat/completions"                      // OpenAI APIのエンドポイントURL
 const OPENAI_API_MODEL = "gpt-3.5-turbo-0125"                                                 // 使用するGPTモデル

 const WEBHOOK_URL = "" // 上でコピーしたWebhook urlを貼り付けてください。                         // Discord Webhook URL
 
 // 検索クエリ
    const QUERY = "検索したいキーワードを入力する";                                                 // arXivで検索するクエリ
    // 検索対象日数
    const TERM = 1;                                                                            // 検索対象の日数
    // 検索時のヒット論文で要約する論文の本数の上限
    const MAX_PAPER_COUNT = 3;                                                                 // 要約する論文の最大数
    // ChatGPT に渡す命令
    const PROMPT_PREFIX = "あなたは情報教育、テクノロジーに詳しい教師です。\n以下の論文を、タイトルと要約の2点をそれぞれ改行で分けて、専門用語を使わず、簡素で平易な日本語で説明してください。\n要点は箇条書きでお願いします。\n## 出力形式 \nタイトル: {タイトル}\n要約: {要約}";  // ChatGPTに渡すプロンプト
 
 function getDateBeforeDays(days) {
   const date = new Date();                                                                    // 現在の日付を取得
   date.setMinutes(date.getMinutes() - date.getTimezoneOffset());                              // タイムゾーンオフセットを調整
   date.setDate(date.getDate() - days);                                                        // 指定された日数前の日付に設定
   return date.toISOString().split("T")[0];                                                    // YYYY-MM-DD形式の日付文字列を返す
 }
 
 function getArxivPapers(query, fromDate, toDate) { 
   const options = {
     muteHttpExceptions: true,                                                                 // HTTPエラーを無視するオプション
   };
   const url = `http://export.arxiv.org/api/query?search_query=${query}&start=0&max_results=20&sortBy=submittedDate&sortOrder=descending`  // arXiv APIのURL
   const xmlText = UrlFetchApp.fetch(url, options).getContentText();                           // arXiv APIからXMLを取得
   const document = XmlService.parse(xmlText);                                                 // XMLをパース

   const root = document.getRootElement();                                                     // ルート要素を取得
   const entries = root.getChildren("entry", XmlService.getNamespace("http://www.w3.org/2005/Atom"));  // entry要素を取得
   
   const papers = entries
     .map((entry) => {
       const title = entry.getChild("title", XmlService.getNamespace("http://www.w3.org/2005/Atom")).getText();        // タイトルを取得
       const abstract = entry.getChild("summary", XmlService.getNamespace("http://www.w3.org/2005/Atom")).getText();   // 要約を取得
       const id = entry.getChild("id", XmlService.getNamespace("http://www.w3.org/2005/Atom")).getText();             // IDを取得
       const published = entry.getChild("published", XmlService.getNamespace("http://www.w3.org/2005/Atom")).getText();// 公開日を取得
       return {
         title: title,
         abstract: abstract,
         url: id,
         published: published,
       };
     }).filter((paper) => {
       const publishedDate = new Date(paper.published);                                       // 公開日をDate型に変換
       const from = new Date(fromDate);                                                       // 検索開始日をDate型に変換
       const to = new Date(toDate);                                                           // 検索終了日をDate型に変換
       to.setDate(to.getDate() + 1);                                                          // 検索終了日の翌日に設定
       return publishedDate >= from && publishedDate < to;                                    // 公開日が検索期間内かどうかを判定
     });
 
   console.log(`フィルタリング後の論文数: ${papers.length}`);                                            // フィルタリング後の論文数をログ出力
   return papers;                                                                             // 論文情報の配列を返す
 }
 
 function callChatGPT(input) {
     const messages = [
         {
             role: "user",
             content: PROMPT_PREFIX + "\n" + input,                                           // ユーザーの入力とプロンプトを結合
         },
     ];
  
     const options = {
         "method": "post",                                                                    // POSTリクエスト
         "headers": {
             "Authorization": `Bearer ${OPENAI_API_KEY}`,                                     // APIキーを認証ヘッダーに設定
             "Content-Type": "application/json",                                              // リクエストボディのContent-Typeを設定
         },
         "payload": JSON.stringify({
             model: OPENAI_API_MODEL,                                                         // 使用するGPTモデルを指定
             messages,                                                                        // メッセージを指定
         }),
     };
 
     return JSON.parse(UrlFetchApp.fetch(OPENAI_API_ENDPOINT, options).getContentText());      // OpenAI APIにリクエストを送信し、レスポンスをJSONとしてパース
 }
 
  function main() {
    if (!OPENAI_API_KEY) {
      console.log("ERROR: OPEN_API_KEY を指定してください");                                         // APIキーが設定されていない場合はエラーメッセージをログ出力
      return;
    }
  
    const fromDate = getDateBeforeDays(TERM);                                                 // 検索開始日を取得
    const toDate = getDateBeforeDays(0);                                                      // 検索終了日を取得
    console.log(`検索範囲の開始日: ${fromDate}`);                                                   // 検索開始日をログ出力
    console.log(`検索範囲の終了日: ${toDate}`);                                                     // 検索終了日をログ出力
    const arxivPapers = getArxivPapers(QUERY, fromDate, toDate);                              // arXivから論文情報を取得
    console.log(`取得された論文数: ${arxivPapers.length}`);                                          // 取得された論文数をログ出力
    const papers = arxivPapers;                                                               // 論文情報を変数に代入
    let output = "新着論文のお知らせ\n\n";                                                           // 出力メッセージの先頭部分
    let paperCount = 0;                                                                       // 処理済み論文数のカウンター
  
    if (papers.length === 0) {
      output += "指定された期間内に、検索クエリに一致する新しい論文は見つかりませんでした。\n\n";                         // 該当する論文がない場合のメッセージ
    } else {
      for (const paper of papers) {
        if (++paperCount > MAX_PAPER_COUNT) break;                                            // 処理済み論文数が上限に達した場合はループを抜ける
        console.log(`論文${paperCount}: ${paper.title}`);                                        // 処理中の論文のタイトルをログ出力
        const title = paper.title;                                                            // 論文のタイトル
        const abstract = paper.abstract;                                                      // 論文の要約
        const url = paper.url;                                                                // 論文のURL
        const input = "\n" + "title: " + title + "\n" + "abstract: " + abstract;              // ChatGPTへの入力
        const res = callChatGPT(input);                                                       // ChatGPTにリクエストを送信
        console.log(`ChatGPTからのレスポンス: ${JSON.stringify(res, null, 2)}`);                      // ChatGPTからのレスポンスをログ出力
        const paragraphs = res.choices.map((c) => c.message.content.trim());                  // レスポンスから段落を抽出
        output += `${paragraphs.join("\n")}\n\n${url}\n\n\n`;                                 // 出力メッセージに段落とURLを追加
      }
    }
  
    output = output.trim();                                                                   // 出力メッセージの前後の空白を削除
    console.log(`最終結果: ${output}`);                                                           // 最終的な出力メッセージをログ出力
    
    const payload =
       {
         "content": output                                                                    // Discordに送信するメッセージ
       };
     
    const options =
       {
         "method": "post",                                                                    // POSTリクエスト
         "payload": payload                                                                   // リクエストボディ
       };
     
    UrlFetchApp.fetch(WEBHOOK_URL, options);                                                  // DiscordのWebhook URLにリクエストを送信
  }

OpenAIのAPIキーが必要になりますので、キーを取得していない方はhttps://platform.openai.com/account/api-keys より取得しましょう。
OpenAIのAPIキーは左の歯車アイコンの「プロジェクトの設定」にてOPENAI_API_KEYをプロパティとして作成して取得したキーを設定してください。

APIキーの設定までできたらテスト実行をしてみましょう。
main関数を指定して実行してみましょう。

割とmainに変えるのを忘れて実行してしまうことがある

上手く動作すると、こんな感じに正常終了しDiscordに投稿されます。
今回は期間内に指定したキーワードの論文が見つからなかったので、その旨の通知が表示されています。

論文が取得できるとこのようなメッセージが投稿されます。

スクリプト実行時に権限を求められますが、「このアプリは確認されていません」とびっくりするメッセージが表示されることがあります。その際は以下のサイトに書いてある通り、「詳細」→「メッセージダイアログ(安全ではないページ)に移動」と進んでいくと実行できます。

定期実行の設定しよう!

自動的に実行してもらうためにタイマーを左側の時計アイコンの「トリガー」で設定します。
今回は毎日7~8時に実行して通知してもらうように設定し、保存します。

デプロイしよう!

ここまで来たら準備は完了ですので、あとはデプロイするだけです。「デプロイ」→「新しいデプロイ」を選択し説明文を追加してデプロイしたら完了です。

定期実行の確認しよう!

最後に、設定した時間にDicordに投稿されたら成功です!

7~8時の間に無事投稿されました

いかがでしたでしょうか?
筆者はGASを使うのは初めてでしたが簡単に実装することができました。
次はTwitterやNoteからの投稿情報をDiscordに投稿する仕組みを作成したいと思います。

ここまでお読みいただき、ありがとうございました!