ChatGPTでarXiv論文紹介を毎朝LINEへ配信

# はじめに
2023/4/9からChatGPTでarXiv論文紹介を自分宛に毎朝3件配信しています。
2ヶ月運用してみると、時々興味深いタイトルを配信してくれて楽しませてくれるので今更ながらソース公開してみます。とは言っても、下記の２記事の内容ほぼそのままなんですけど。
機能の違いは、LINEにはタイトルのみを、メールには概要の通知を行う点。

LINEでタイトル見て興味をもったらメールを読みに行く、という運用です。これが何気に便利です。ただし、タイトルだけの翻訳精度は高くないので、興味を持てるかどうかの判断材料程度です。

https://twitter.com/niniziv/status/1638155751515631617

https://note.com/atari_ryuma/n/ncbcfcd703565

# 使い方
使い方も同じ。
1. OpenAIのAPI keyを取得する → https://platform.openai.com/account/api-keys
2. Google App Scriptの新しいプロジェクトを作成する。 → https://script.google.com/home/
3. 作成したプロジェクトを開き、コード全文をエディタにコピペして下記を埋める。
   OPENAI_API_KEY　： OpenAIの API Key
   EMAIL_RECIPIENT : 通知先メールアドレス
   LINE_TOKEN      : LINE Notify Token
4. この時点でコードエディタ画面上部の実行ボタン（再生マーク）を押して正しく実行できるか確認しておくと良い
5. 左カラムからトリガー設定画面（目覚ましアイコン）を開き、よしなに自動実行を設定する。


LINE通知用のtokenは下記で取得してください。
https://notify-bot.line.me/my/
参考：https://tetsuooo.net/gas/2739/


# Source

GASです。

```JavaScript:code.gas
// OpenAI の API keyを入力↓
const OPENAI_API_KEY = 'xxxxxxxxxxxxxxxxx';
// ChatGPT に渡すプロンプト（適宜変えること）
const PROMPT_PREFIX = "あなたは機械学習と機械工学と自然言語処理に精通した研究者で、論文を簡潔に要約することに優れています。以下の論文を、タイトルと要約の2点をそれぞれ改行で分けて日本語で説明してください。要点は箇条書きで4-8行程度にまとめること。";

// 結果メールの送信先を入力↓
const EMAIL_RECIPIENT = "xxxxx@xxxxx.com";
// 結果メールのタイトル
const EMAIL_SUBJECT = "arXivの新着論文のお知らせ";
// 結果メールの送信者の名前
const EMAIL_SENDER = "arXiv要約Bot";
// LINE Notiy Token
const LINE_TOKEN = 'xxxxxxxxxxxxxxxxxxxx';

// 検索クエリを定義（適宜修正する）
// 例：cond-mat.mtrl-sciカテゴリの論文のうち、タイトルか要旨にmachine learningもしくはdeep learningを含む論文を検索する場合↓
const ARXIV_QUERY = "cat:cs.AI OR cat:cs.SY AND (ti:machine learning OR ti:deep learning OR abs:machine learning OR abs:deep learning)";
const MAX_PAPER_COUNT = 3; // 最大結果数
const ARXIV_API_URL = `http://export.arxiv.org/api/query`;
const ARXIV_TERM = 7; // 過去何日分の記事を検索するか

function main() {
  if (!OPENAI_API_KEY) {
    console.log("ERROR: OPEN_API_KEY を指定してください");
    return;
  }

  const yesterday = new Date();
  yesterday.setDate(yesterday.getDate() - ARXIV_TERM);

  const { entries, namespace } = getEntriesOn(yesterday);
  let output = "arXivの新着論文のお知らせ\n\n";
  let output_for_line = output;
  let paperCount = 0;
  
  entries.forEach(entry => {
    const arxivUrl = entry.getChild('id', namespace).getText();
    const title = entry.getChild("title", namespace).getText();
    if (++paperCount > MAX_PAPER_COUNT) return;
    const authors = entry.getChildren("author", namespace);
    const authorNames = authors.map(author => author.getChild("name", namespace).getText()).join(", ");
    const summary = entry.getChild("summary", namespace).getText();
    const input = "\n" + "title: " + title + "\n" + "summary: " + summary;
    const res = callChatGPT([
      {
        role: "user",
        content: PROMPT_PREFIX + "\n" + input,
      },
    ]);

    if ("error" in res) {
      console.log('Error:' + res.error.message);
      return;
    }

    const paragraphs = res.choices.map((c) => c.message.content.trim());
    output += `Title: ${title}\nAuthors: ${authorNames}\n\n ${paragraphs.join("\n")}\n\n${arxivUrl}\n------------------------\n\n\n`;
    output_for_line += '\n' + paragraphs[0].split("\n")[0] + '\n';
  });

  output = output.trim();
  console.log(output);
  sendEmail(output);

  output_for_line = output_for_line.trim();
  sendLineNotify(output_for_line);
}

function toYYYYMMDD(date) {
  return [date.getFullYear(), date.getMonth() + 1, date.getDate()].join("-");
}

function getEntriesOn(date) {
  const url = `${ARXIV_API_URL}?search_query=${ARXIV_QUERY}&sortBy=lastUpdatedDate&sortOrder=descending&max_results=${MAX_PAPER_COUNT}&start=0&start=0&submitted_date[from]=${toYYYYMMDD(date)}&submitted_date[to]=${toYYYYMMDD(date)}`;
  const feed = UrlFetchApp.fetch(url).getContentText();
  const xml = UrlFetchApp.fetch(url).getContentText();
  const xmlDocs = XmlService.parse(xml);
  const namespace = XmlService.getNamespace('', 'http://www.w3.org/2005/Atom');
  const entries = xmlDocs.getRootElement().getChildren('entry', namespace);
  return { entries, namespace };
}


function callChatGPT(messages) {
    const url = "https://api.openai.com/v1/chat/completions";

    const options = {
        "method": "post",
        "headers": {
            Authorization: `Bearer ${OPENAI_API_KEY}`,
            "Content-Type": "application/json",
        },
        "muteHttpExceptions": true,
        "payload": JSON.stringify({
            model: "gpt-3.5-turbo",
            messages,
        }),
    };

    return JSON.parse(UrlFetchApp.fetch(url, options).getContentText());
}

function sendEmail(body) {
    const recipient = EMAIL_RECIPIENT;
    const subject = EMAIL_SUBJECT;
    const options = { name: EMAIL_SENDER };
    GmailApp.sendEmail(recipient, subject, body, options);
}

function sendLineNotify(body) {
  const lineNotifyApi = 'https://notify-api.line.me/api/notify';

  const options =
   {
      "method"  : "post",
      "payload" : {"message": body},
      "headers" : {"Authorization":"Bearer " + LINE_TOKEN},
      "muteHttpExceptions": true
   };

   UrlFetchApp.fetch(lineNotifyApi, options);
}
```

# LINE, Mail sample

LINEへの通知はこんな感じ。
![line.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82958/3b6a33ed-b2ae-b516-555b-e969c1d35184.jpeg)


メールにはこんな感じ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82958/39fde286-b267-7dcb-708b-07132e0e43b1.png)

* 期待せずに気楽に始めたけど、安定して毎日送ってきてくれるし時々興味を引く。
* もう少しフィルタ条件を工夫してみるとよさそう。
* 説明部分はすべて人任せで手抜きな記事だな。

おわり。