
[コード公開] GAS x Zapier x LLMで今日のニュースを自動でサマリーする
こんにちは、note AI creativeの田中です。
「今日話題になったAI系のニュースを自動でサマリーしてお知らせする仕組み」を作ったので作り方を解説します。Zapierの設定やGASのコードも可能な範囲で公開しています。


全体の構成
全体の流れは以下の図のとおりです。

①〜③:RSSでニュースを取得して、各ニュースの要約を作成
④:GASで今日のニュース要約を作成
⑤:Zapierで今日のニュース要約をSlackにポスト
1. ZapierでRSS購読して要約&スプシに記載

以下はZapierの設定内容です。

ステップ1のRSS by Zapierは https://b.hatena.ne.jp/hotentry/it.rss を購読している。
ステップ2は、「Raw Subject」にAI関連のキーワードが入っているかで判定している。

ステップ4の「Webhook by Zapier」は社内で使っているAzureのエンドポイントにPOSTリクエストしている。OpenAIのAPIを使っても同じことができる。ただし、Zapierはタイムアウトが30秒程度と厳しいため、GPT-4などは使えない。GPT-4oやGPT-4o-miniなど推奨。
以下は使用しているプロンプト
以下の記事のContentを日本語でFormatに従い要約してください。
要約は要点を5点に整理して下さい。文書は日本語で要約して下さい。
<Format>
タイトル:{日本語にした記事のタイトル}
1. *要約見出し1*: 要約のポイント1つ目。
2. *要約見出し2*: 要約のポイント2つ目。
3. *要約見出し3*: 要約のポイント3つ目。
4. *要約見出し4*: 要約のポイント4つ目。
5. *要約見出し5*: 要約のポイント5つ目。
</Format>
<Content>
タイトル:{title}
{content}
</Content>
ステップ7の「今日の日付を取得」はJavaScriptのコードで今日の日付を取得している(スプシ書き込み用)
// 現在の日付を取得
var date = new Date();
// 年を取得
var year = date.getFullYear();
// 月を取得(0から11の値を返すため、1を足す)
var month = date.getMonth() + 1;
// 日を取得
var day = date.getDate();
// 月が1桁の場合は先頭に0を追加
if (month < 10) {
month = "0" + month;
}
// 日が1桁の場合は先頭に0を追加
if (day < 10) {
day = "0" + day;
}
// 出力する日付の形式を指定(yyyy/mm/dd)
var outputDate = year + "/" + month + "/" + day;
// 出力結果をオブジェクトとしてまとめる
output = { Date: outputDate };
ステップ8では、結果をスプレッドシートに書き込んでいる。

このZapierを稼働させていると、スプレッドシート上に毎日複数件のニュース記事が貯まるようになってくる。

2. GASで今日のニュース要約を作成

ここでは、スプレッドシートに溜まった記事群を1日単位でまとめて、今日話題になったトピックやそれらのサマリーをLLMで要約します。
以下は実際に動かしているコードです。
/**
* メイン関数。スプレッドシートからデータを取得し、加工して結果をシートに書き込む。
*/
function processAndCallClaude() {
var spreadsheetId = '<ここにシートID>';
var sheetName = 'シート1';
var data = getSpreadsheetData(spreadsheetId, sheetName);
var headers = getHeaders(data);
var today = getCurrentDate();
var { summaries, urls, titles } = extractData(data, headers, today);
var summaryText = formatSummaryText(summaries);
var urlTitleText = formatUrlTitleText(urls, titles);
var abstractPromptText = getAbstractPrompt();
var finalAbstractText = replacePlaceholders(abstractPromptText, { summary: summaryText, today: today });
var abstractResult = callClaude(finalAbstractText, "claude-3-5-sonnet-20240620");
abstractResult = formatListText(abstractResult);
var detailPromptText = getDetailPrompt();
var finalDetailText = replacePlaceholders(detailPromptText, { news_list: summaryText, abstract: abstractResult });
var summaryResult = callClaude(finalDetailText, "claude-3-5-sonnet-20240620");
summaryResult = formatListText(summaryResult);
writeResultToDailySummary(today, abstractResult, urlTitleText, summaryResult);
}
/**
* スプレッドシートからデータを取得する。
* @param {string} spreadsheetId - スプレッドシートのID
* @param {string} sheetName - シートの名前
* @return {Array<Array<string>>} スプレッドシートのデータ
*/
function getSpreadsheetData(spreadsheetId, sheetName) {
var sheet = SpreadsheetApp.openById(spreadsheetId).getSheetByName(sheetName);
return sheet.getDataRange().getValues();
}
/**
* スプレッドシートのヘッダー行を取得する。
* @param {Array<Array<string>>} data - スプレッドシートのデータ
* @return {Array<string>} ヘッダー行
*/
function getHeaders(data) {
return data[0];
}
/**
* 「daily summary」シートに結果を書き込む
* @param {string} date - 今日の日付
* @param {string} abstract - abstractの内容
* @param {string} links - linksの内容
* @param {string} summary - summaryの内容
*/
function writeResultToDailySummary(date, abstract, links, summary) {
var spreadsheetId = '<ここにシートID>';
var sheetName = 'daily summary';
var sheet = SpreadsheetApp.openById(spreadsheetId).getSheetByName(sheetName);
if (sheet.getRange('A2').getValue() || sheet.getRange('B2').getValue()) {
sheet.insertRowsBefore(2, 1);
}
sheet.getRange('A2').setValue(date);
var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
var abstractColumnIndex = headers.indexOf('abstract') + 1;
sheet.getRange(2, abstractColumnIndex).setValue(abstract);
var linksColumnIndex = headers.indexOf('links') + 1;
sheet.getRange(2, linksColumnIndex).setValue(links);
var summaryColumnIndex = headers.indexOf('summary') + 1;
sheet.getRange(2, summaryColumnIndex).setValue(summary);
}
/**
* 今日の日付を取得する。
* @return {string} 今日の日付(yyyy/MM/dd形式)
*/
function getCurrentDate() {
var today = new Date();
var year = today.getFullYear();
var month = ('0' + (today.getMonth() + 1)).slice(-2);
var day = ('0' + today.getDate()).slice(-2);
return year + '/' + month + '/' + day;
}
/**
* データを抽出する。
* @param {Array<Array<string>>} data - スプレッドシートのデータ
* @param {Array<string>} headers - ヘッダー行
* @param {string} today - 今日の日付
* @return {Object} 抽出されたデータ(summaries、urls、titles)
*/
function extractData(data, headers, today) {
var dateColumnIndex = headers.indexOf('date');
var summaryColumnIndex = headers.indexOf('summary');
var sourceColumnIndex = headers.indexOf('source');
var urlColumnIndex = headers.indexOf('URL');
var titleColumnIndex = headers.indexOf('title');
if (dateColumnIndex === -1 || summaryColumnIndex === -1 || sourceColumnIndex === -1 || urlColumnIndex === -1 || titleColumnIndex === -1) {
Logger.log('必要なヘッダーが見つかりません。');
return;
}
var summaries = [];
var urls = [];
var titles = [];
for (var i = 1; i < data.length; i++) {
var dateCell = data[i][dateColumnIndex];
var formattedDate = Utilities.formatDate(new Date(dateCell), Session.getScriptTimeZone(), 'yyyy/MM/dd');
var sourceCell = data[i][sourceColumnIndex];
if (formattedDate === today && sourceCell === "hatena bookmark") {
summaries.push(data[i][summaryColumnIndex]);
urls.push(data[i][urlColumnIndex]);
titles.push(data[i][titleColumnIndex]);
}
}
return { summaries, urls, titles };
}
/**
* summary列の値をフォーマットする。
* @param {Array<string>} summaries - summary列の値
* @return {string} フォーマットされたsummaryテキスト
*/
function formatSummaryText(summaries) {
return summaries.join('\n\n---\n\n');
}
/**
* URLとtitleをフォーマットする。
* @param {Array<string>} urls - URLのリスト
* @param {Array<string>} titles - タイトルのリスト
* @return {string} フォーマットされたURLとタイトルのテキスト
*/
function formatUrlTitleText(urls, titles) {
return '*:link: URLリスト*\n' + urls.map((url, index) => '・<' + url + '|' + titles[index] + '>').join('\n');
}
/**
* 改行を調整してリスト間の改行を詰める関数
* @param {string} text - フォーマット前のテキスト
* @return {string} フォーマット後のテキスト
*/
function formatListText(text) {
return text.replace(/(\n\s*・)/g, "\n・").replace(/^\s*・/, "・");
}
/**
* テキストのプレースホルダーを置換する。
* @param {string} text - テキスト
* @param {Object} replacements - プレースホルダーと置換値のペア
* @return {string} プレースホルダーが置換されたテキスト
*/
function replacePlaceholders(text, replacements) {
for (var placeholder in replacements) {
var value = replacements[placeholder];
var regex = new RegExp('{' + placeholder + '}', 'g');
text = text.replace(regex, value);
}
return text;
}
function getAbstractPrompt() {
return `
複数のAIニュース記事の要約から、話題になっったトピックを箇条書きで書き出して下さい。
<ai_news_summaries>
{summary}
</ai_news_summaries>
### 手順
これらの要約を注意深く読み、以下の手順に従って分析してください:
1. 各要約に含まれる主要なトピックや出来事を特定します。
2. 影響力のあるトピックを優先順位付けします。
### 出力形式
出力は箇条書きのみで、前後に文章は含めないで下さい。
・[トピック1]
・[トピック2]
・[トピック3]
・[トピック4]
・[トピック5]
...
### 出力例
・「LinguaLink」AI、100言語リアルタイム同時通訳を実現
・AIスタートアップ Neurotech、脳波解析技術で1億ドルの資金調達に成功
・日本政府、AI開発に関する新規制法案を可決
・「ClimateOracle」AI、長期気候変動予測の精度を飛躍的に向上
・米国で自動運転AI搭載車の公道走行規制が緩和
`
}
function getDetailPrompt() {
return `
あなたは今日ネットで話題になったニュース記事を要約する任務を担当します。以下の指示に従って、各ニュース記事の内容を簡潔にまとめてください。
まず、今日話題になったニュース記事の一覧を箇条書きで示します:
<news_list>
{news_list}
</news_list>
次に、これらのニュース記事に関する全体的なトピックリストを提供します:
<topics>
{abstract}
</topics>
### 手順
各ニュース記事について、以下の手順で要約してください:
1. トピックの構成に沿って要約を作成してください。
2. トピックよりも詳細なレベルで情報を提供してください。
3. 各要約は約100文字程度になるようにしてください。
4. 重要な事実、数字、引用などを含めて、記事の本質を捉えてください。
### 出力形式
出力は箇条書きのみで、前後に文章は含めないで下さい。
・[トピック1詳細説明]
・[トピック2詳細説明]
・[トピック3詳細説明]
・[トピック4詳細説明]
・[トピック5詳細説明]
...
### 出力例
1. 多言語コミュニケーション革命の幕開けか。AI企業TechLingua社が開発した「LinguaLink」AIが、100言語のリアルタイム同時通訳に成功。音声認識、自然言語処理、音声合成を統合し、0.5秒以下の遅延で高精度な通訳を実現。国際会議やグローバルビジネスでの活用が期待される一方、通訳者の雇用への影響も懸念されている。
2. 脳科学とAIの融合で次世代インターフェースの開発へ。AIスタートアップNeurotech社が脳波解析技術により1億ドルの資金調達を達成。同社の技術は、思考だけで機器を操作する「ブレイン・コンピューター・インターフェース」の実用化を目指す。医療やエンターテインメント分野での応用が期待されるが、プライバシーの問題も指摘されている。
3. AI開発の適正化へ向けて一歩前進。日本政府がAI開発に関する新規制法案を可決。AIの透明性、説明責任、公平性を確保するための指針を策定。個人情報保護やAI利用の倫理的側面に焦点を当て、産業の健全な発展と個人の権利保護の両立を目指す。法案は来年4月より施行予定だが、技術進歩に対する柔軟な対応が課題。
4. 気候変動対策に新たな光明。環境テック企業GreenAI社の「ClimateOracle」AIが、長期気候変動予測の精度を従来比50%向上させることに成功。膨大な気象データと地球科学モデルを統合し、複雑な気候システムをより正確に予測。政策立案者や企業の長期的な環境戦略に貢献すると期待されるが、予測結果の解釈には慎重な姿勢も求められる。
5. 自動運転技術の実用化が加速。米運輸省が自動運転AI搭載車の公道走行規制を緩和。一定の安全基準を満たせば、ステアリングやペダルのない完全自動運転車の公道走行が可能に。交通事故削減や移動弱者支援への期待が高まる一方、サイバーセキュリティや責任問題など、新たな課題への対応も必要とされている。
`
}
function callClaude(prompt, model) {
var scriptProperties = PropertiesService.getScriptProperties();
var apiKey = scriptProperties.getProperty('CLAUDE_API_KEY');
var apiUrl = "https://api.anthropic.com/v1/messages";
var headers = {
"x-api-key": apiKey,
"anthropic-version": "2023-06-01",
"content-type": "application/json"
};
var data = {
"model": model,
"max_tokens": 4000,
"messages": [
{"role": "user", "content": prompt}
]
};
var options = {
"method": "post",
"headers": headers,
"payload": JSON.stringify(data)
};
var response = UrlFetchApp.fetch(apiUrl, options);
var responseText = response.getContentText();
// レスポンスをパースして content 配列内の text 部分を取得
var responseJson = JSON.parse(responseText);
var content = responseJson.content;
var text = content[0].text;
return text;
}
これをトリガーで毎日午後4-5時のあいだに実行するようにしています。

実行されると以下のように「daily summary」シートにサマリーが出力されます。「date」は今日の日付、「abstract」はトピックの箇条書きで、「summary」はより詳細な要約が入っています。「links」はSlack用書式にフォーマットされたリンク集が入っています。

3. 今日のサマリーをSlackにポストする

ここでは、スプレッドシート上から今日のサマリーを抽出してSlackにポストします。チャンネルにまずトピックだけ投下して、スレに畳む形で詳細とリンク集を流しています。


以下はZapierの設定です。




▼noteの技術記事が読みたい方はこちら