虎の穴ラボ技術ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

OpenAI × GASでブログ要約をSlackに自動投稿!


こんにちは、A.M.です。
この記事では、OpenAI(ChatGPT)とGoogle Apps Script(以降GAS)を使用した、ブログの新規投稿を要約してSlackに自動投稿する方法をご紹介したいと思います。 虎の穴ラボの社内でも、メンバー向けに投稿された記事を紹介するために使用しています。

目次

やりたいこと

  • ブログに新規投稿されたら
  • 記事の内容を要約して
  • Slackに投稿する

実現方法

  • RSSで新規記事を取得
    • 最新記事に絞るために最終チェック日時を保持
  • OpenAIのAPIで記事の要約を作成
  • Slackに投稿

実際の投稿イメージ

社内のSlackに実際に投稿されている内容です。
気軽に記事を読んでもらえるように、あえて楽しそうな表現にしています。

やり方

前提

以下の2つを取得済みであること。

  • OpenAIのAPIキー
    • /v1/chat/completionsAPIのWrite権限を付与したAPIキーを作成してください。
  • SlackのWebhook URL

※本記事では取得方法については触れませんのでご了承ください。

GASのプロジェクト作成&設定

まず、script.google.comにアクセスし、「+新しいプロジェクト」ボタンをクリックします。
無題のプロジェクトが作成されるので、タイトルを変更しておきましょう。


続いて、サイドメニューから「プロジェクトの設定」を選択して設定画面を開きます。


設定画面の下の方にある「スクリプト プロパティを追加」ボタンを押し、以下の内容を追加します。

プロパティ 備考
RSS_URL https://toranoana-lab.hatenablog.com/rss 虎の穴ラボ技術ブログのRSSです。他ブログの記事を取得したい場合は適宜変更してください。
CHAT_GPT_API_KEY sk-xxxx 取得したOpenAIのAPIキーを入力してください。
CHAT_GPT_MODEL_TYPE gpt-4o 2024年12月時点で一番要約に向いているように思います。費用やレスポンス速度が気になる場合はgpt-4o miniでもよいかもしれません。
SLACK_WEBHOOK_URL https://hooks.slack.com/services/Txx/Bxx/dummy 取得したSlackのWebhook URLを入力してください。
lastChecked Mon, 02 Dec 2024 03:00:00 GMT 最終チェック日時です。初期値は任意ですが、日時が古いと大量の記事が投稿対象になってしまう場合があるので注意してください。例として「2024-12-02 12:00:00 JST」の場合の入力値を記載しています。

追加が終わったら、「スクリプト プロパティを保存」ボタンを押してください。
以上でプロジェクトの準備は完了です。

GASの実装

プロジェクトのサイドメニューから、「エディタ」に戻り、以下のようにGASを実装します。

▼コード.gs

// 環境変数の定義。プロジェクトの設定に追加したプロパティを読み込み。
const RSS_URL = PropertiesService.getScriptProperties().getProperty('RSS_URL');
const CHAT_GPT_API_KEY = PropertiesService.getScriptProperties().getProperty('CHAT_GPT_API_KEY');
const CHAT_GPT_MODEL_TYPE = PropertiesService.getScriptProperties().getProperty('CHAT_GPT_MODEL_TYPE');
const SLACK_WEBHOOK_URL = PropertiesService.getScriptProperties().getProperty('SLACK_WEBHOOK_URL');
// 記事要約のプロンプト
const SYSTEM_PROMPT = `
あなたはプロの技術ブログ運用エンジニアです。
ユーザーから渡された【技術ブログの内容】を踏まえて、【条件】に従って要約文を【出力フォーマット】に沿って作成してください。
### 条件
- 要約は3項目のリスト形式で出力してください
- 技術ブログの伝えようとしていること、優れた点などを紹介すること
- 要約を見た第三者エンジニアが技術ブログを読みたくなるようなものにすること
- 要約は3項目のリスト形式では、それぞれの要約文章中に必ず絵文字を使うこと
- 楽しげで賑やかな文章にすること
- 要約の1項目は100文字以上130文字以内にしてください。
- マークダウン形式は使わないでください
### 出力フォーマット





`;

// 新しい記事をチェックする
function checkNewPosts() {
  const lastDate = PropertiesService.getScriptProperties().getProperty('lastChecked');
  const rss = UrlFetchApp.fetch(RSS_URL).getContentText();
  const document = XmlService.parse(rss);
  const entries = document.getRootElement().getChild('channel').getChildren('item');

  const posts = [];
  entries.forEach(entry => {
    const pubDate = entry.getChild('pubDate').getText();
    const title = entry.getChild('title').getText();
    const link = entry.getChild('link').getText();
    const content = entry.getChild('description').getText();

    if (new Date(pubDate) > new Date(lastDate)) {
      // 最終チェック日時以降の新規記事があれば投稿対象として配列に追加
      posts.push({ title, link, content });
    }
  });

  if (posts.length > 0) {
    // lastCheckedを更新
    PropertiesService.getScriptProperties().setProperty('lastChecked', new Date().toUTCString());
    sendToSlack(posts);
  }
}

function summarizeContent(content) {
  return callOpenAiApi(content)
}

function sendToSlack(posts) {
  posts.forEach(post => {
    const content = `${post.title}\n${post.content}`
    const message = {
      "text": `<${post.link}|${post.title}> が投稿されました!

${summarizeContent(content)}

`}

    UrlFetchApp.fetch(SLACK_WEBHOOK_URL, {
      'method': 'post',
      'contentType': 'application/json',
      'payload': JSON.stringify(message)
    });
  });
}

function callOpenAiApi(content) {
  const url = 'https://api.openai.com/v1/chat/completions';
  const options = {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${CHAT_GPT_API_KEY}`,
      'Content-Type': 'application/json'
    },
    payload: JSON.stringify({
      "model": CHAT_GPT_MODEL_TYPE,
      "temperature": 0, // 必要に応じて調整してください
      "messages": [
        {
          "role": "system",
          'content': SYSTEM_PROMPT
        },
        {
          "role": "user",
          'content': `### 技術ブログの内容\n${content}`
        }
      ]
    })
  };
  const response = UrlFetchApp.fetch(url, options);
  const data = JSON.parse(response.getContentText());
  return data.choices[0].message.content;
}

解説

SYSTEM_PROMPTについて

プロンプトの内容は上記のコード内に定義している通りですが、出力されたテキストをそのまま投稿に使えるように、出力フォーマットを指定しているところがポイントです。

### 出力フォーマット
▷

▷ 

▷ 

checkNewPosts関数

新規投稿をチェックする関数です。
最終チェック日時(lastChecked)以降に公開された新しい記事を投稿対象としています。

callOpenAiApi関数

OpenAI APIの呼び出しを行う関数です。
ここでプロンプトと記事の内容をOpenAIに送信して、記事の要約を作成してもらっています。
今回はChatGPTのようにプロンプトを渡してテキストの生成を行うので、Chat Completions APIを使用しています。
APIの仕様等についてもっと詳しく知りたい場合は公式リファレンスを参照してください。

トリガーの設定

GASの実装ができたら、最新記事のチェックを定期的に実行するようにトリガーを設定します。
プロジェクトのサイドメニューから「トリガー」を選択します。


画面右下の「+トリガーを追加」ボタンをクリックします。


次のように設定(基本的にデフォルトでOK)して「保存」ボタンを押します。

項目 設定値(デフォルト値)
実行する関数を選択 checkNewPosts
実行するデプロイを選択 Head
イベントのソースを選択 時間主導型
時間ベースのトリガーのタイプを選択 時間ベースのタイマー
時間の間隔を選択(時間) 1時間おき

※実行間隔を変更したい場合は「時間ベースのトリガーのタイプを選択」や「時間の間隔を選択(時間)」を変更してください。

注意事項

OpenAI APIの利用は有料です。
1リクエストあたりの費用は高くはないですが、リクエスト数が増えると高額になる可能性もあります。
対象の記事数が大量にならないように、lastCheckedの初期値は注意して設定してください。

まとめ

OpenAIとGASを組み合わせてブログ記事の要約をSlackに投稿する方法についてご紹介しました。
これをベースに、OpenAIの代わりに以前の記事で紹介したGoogleのGemini APIを使って記事を要約してみるのも面白そうですね。GeminiであればレスポンスにJSON形式を指定できるので扱いやすそうです。
また、たとえば「記事が公開されたら内容を要約してSNS等に自動投稿する」といったことも実現できそうなので、今後もいろいろ試してみようと思います。

採用情報

虎の穴ラボでは一緒に働く仲間を募集中です!
この記事を読んで、興味を持っていただけた方はぜひ弊社の採用情報をご覧ください。
toranoana-lab.co.jp