GASでHackerNewsのランキングをChatGPTで要約しSlack通知
概要
今回はHackerNewsのトップ記事をChatGPT APIを使って要約してもらい、Slackに通知するBotをGAS (Google Apps Script) を使って作ってみたいと思います。
また今回モデルを gpt-4o-mini
、temperature: 0.5
で設定していますが、必要に応じて適宜変更してもらえればと思います。
Hacker News API
事前準備
事前にChatGPT APIのAPIキーを発行しておく必要があります。
構成
GASの定期実行を使って、HackerNewsの一覧を取得、GPTで要約 + 翻訳したものをSlackに投稿する。
GAS プロジェクト作成
clasp + Docker 構成でGASプロジェクトを作成して行きたいと思います。
👆をベースに環境構築していきます。
- 最終的な
compose.yml
volumes:
clasp_data:
modules_data:
name: gas_project
services:
app:
build: .
volumes:
- ..:/usr/src
- clasp_data:/home/vscode/clasp
- modules_data:/usr/src/node_modules
command: /bin/sh -c "while sleep 1000; do :; done"
working_dir: /usr/src
environment:
clasp_config_auth: /home/vscode/clasp/.clasprc.json
SCRIPT_ID: ${SCRIPT_ID}
※ 本来は clasp login
すると ~/.clasprc.json
に書き出されるのですが、
clasp_config_auth
を設定する事で場所を変更する事ができます。
postAttach.sh
sudo chown -R vscode /home/vscode/clasp
👆を追加し、/home/vscode/clasp を触れるようにしときます。
- 初期化と必要なパッケージ追加
yarn init
yarn add -D @google/clasp @types/google-apps-script
clasp login
clasp login
を実行し、ログインしときます。
※ 執筆時点ではコンテナ起動し、VSCode内のターミナルで clasp login
をすると自動でホスト側と接続してくれてcallbackがちゃんと返りログインする事ができました。
GAS 作成
今回プロジェクト名を「HackerNewsBot」として作成します。
yarn clasp create --title "HackerNewsBot"
作成された main.ts
とappsscript.json
を src
ディレクトリを作成して移動させときます。
ChatGPT APIを使った簡単なサンプル作成
早速、ChatGPT APIを使ったサンプルを試してみたいと思います。
src/main.ts
を以下に修正します。
import URLFetchRequestOptions = GoogleAppsScript.URL_Fetch.URLFetchRequestOptions;
import HttpMethod = GoogleAppsScript.URL_Fetch.HttpMethod;
const OPENAI_API_PROPERTY_KEY = "openai_api_key";
const OPENAI_API_URL = "https://api.openai.com/v1/chat/completions";
interface Message {
role: string;
content: string;
}
const main = () => {
Logger.log("hello world!");
};
const request = (messages: Message[]) => {
const apiKey = PropertiesService.getScriptProperties().getProperty(
OPENAI_API_PROPERTY_KEY
);
const headers = {
Authorization: `Bearer ${apiKey}`,
"Content-type": "application/json",
};
const options: URLFetchRequestOptions = {
muteHttpExceptions: true,
headers: headers,
method: "POST" as HttpMethod,
payload: JSON.stringify({
model: "gpt-4o-mini",
temperature: 0.5,
messages: messages,
}),
};
const response = JSON.parse(
UrlFetchApp.fetch(OPENAI_API_URL, options).getContentText()
);
return response;
};
const checkGpt = () => {
const messages = [
{
role: "system",
content: "必ず関西弁で答えてください",
},
{ role: "user", content: "日本で最も高い山は?" },
];
const response = request(messages);
Logger.log(response.choices[0].message.content);
};
今回スクリプトプロパティを使ってAPIキーを管理しているので、事前に登録しておきます。
対象のGoogle Apps Scriptを開いて「プロジェクトの設定」>「スクリプト プロパティ」で
- プロパティ: openai_api_key
- 値: APIキー
を登録します。
最後に UrlFetchApp
を使う為に必要な権限をappsscript.json
内の oauthScopes
に設定します。
{
"timeZone": "Asia/Tokyo",
"dependencies": {},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"oauthScopes": [
"https://www.googleapis.com/auth/script.external_request"
]
}
これで準備ができたので、以下コマンドでデプロイします。
clasp push
正しくデプロイされたら
①関数を 「checkGpt」に設定し②実行してやると、実行ログに結果が表示されます 🎉
HackerNews APIを使った簡単なサンプル作成
まずはHackerNewsのAPIをリクエストする専用のクライアントクラスを作成します。
src/hackenews/client.ts
を以下内容で作成します。
import URLFetchRequestOptions = GoogleAppsScript.URL_Fetch.URLFetchRequestOptions;
import HttpMethod = GoogleAppsScript.URL_Fetch.HttpMethod;
const HACKER_NEWS_API_URL = "https://hacker-news.firebaseio.com/v0";
export class HackerNewsClient {
constructor() {}
public topStories() {
const url = `${HACKER_NEWS_API_URL}/topstories.json`;
const response = this.fetch(url) as number[];
response.slice(0, 10).forEach((id: number) => {
const item = this.item(id);
Logger.log(`id: ${id}, link: ${item.url}`);
});
}
public item(id: number) {
const url = `${HACKER_NEWS_API_URL}/item/${id}.json`;
const response = this.fetch(url);
return response;
}
private fetch(url: string) {
const headers = {
"Content-type": "application/json",
};
const options: URLFetchRequestOptions = {
headers: headers,
method: "GET" as HttpMethod,
};
const response = UrlFetchApp.fetch(url, options).getContentText();
const json = JSON.parse(response);
return json;
}
}
次にChatGPT APIの時と同じようにテスト用の checkHackerNews
を作成します。
const checkHackerNews = () => {
const client = new HackerNewsClient();
const topStories = client.topStories();
Logger.log(topStories);
};
先程と同じ様に実行する関数に「checkHackerNews」を選択して実行してやるとログが表示されるかと思います。
HackerNews をChatGPT APIで要約する
いよいよ本題といきたい所ですが、HackerNews APIで返ってくるURLの内容を取得する処理を書きたいと思います。
URLの内容を取得後、パースする為に cheerio をGASでも使えるようにしたライブラリを使って実装していこうと思います。
src/appsscript.json
の dependencies
に以下を追加します。
"dependencies": {
"libraries": [
{
"userSymbol": "Cheerio",
"version": "16",
"libraryId": "1ReeQ6WO8kKNxoaA_O0XEQ589cIrRvEBA9qcWpNqdOP17i47u6N9M5Xh0",
"developmentMode": true
}
]
},
パース専用のclassを src/parser.ts
として作成します。
今回は <main>
タグ > <article>
タグ > <body>
タグの順で存在するタグの中を抽出しています。
import URLFetchRequestOptions = GoogleAppsScript.URL_Fetch.URLFetchRequestOptions;
import HttpMethod = GoogleAppsScript.URL_Fetch.HttpMethod;
export class Parser {
constructor() {}
parse(url: string) {
const options: URLFetchRequestOptions = {
method: "GET" as HttpMethod,
};
try {
const response = UrlFetchApp.fetch(url, options).getContentText();
const $ = Cheerio.load(response);
if ($("main").length > 0) {
return $("main").text();
} else if ($("article").length > 0) {
return $("article").text();
} else {
return $("body").text();
}
} catch (e) {
Logger.log(e);
return "";
}
}
}
この時 Cheerio
をimportせず使っているのでエラーが出るかと思います。なので @types/cheerio
だけインストールしimportしてやります。
yarn add -D @types/cheerio
先ほどの src/parser.ts
にimportを追加
import * as Cheerio from "cheerio";
次にChatGPT API扱う為のclassを作成します。
src/chatgpt/client.ts
を以下内容で作成します。
import URLFetchRequestOptions = GoogleAppsScript.URL_Fetch.URLFetchRequestOptions;
import HttpMethod = GoogleAppsScript.URL_Fetch.HttpMethod;
const OPENAI_API_PROPERTY_KEY = "openai_api_key";
const OPENAI_API_URL = "https://api.openai.com/v1/chat/completions";
interface Message {
role: string;
content: string;
}
export class ChatGptClient {
private _apiKey: string;
constructor() {
this._apiKey = PropertiesService.getScriptProperties().getProperty(
OPENAI_API_PROPERTY_KEY
);
}
summarize(content: string) {
if (!content) return "";
const input = `以下のコンテンツについて、内容を日本語で300文字程度でわかりやすく箇条書きで要約して下さい
========
${content}
========
`;
const messages = [
{
role: "system",
content: "あなたはプロのライターです",
},
{ role: "user", content: input },
];
const response = this.request(messages);
return response?.choices[0].message.content ?? "";
}
request(messages: Message[]) {
const headers = {
Authorization: `Bearer ${this._apiKey}`,
"Content-type": "application/json",
};
const options: URLFetchRequestOptions = {
headers: headers,
method: "POST" as HttpMethod,
payload: JSON.stringify({
model: "gpt-4o-mini",
temperature: 0.5,
messages: messages,
}),
};
try {
const response = JSON.parse(
UrlFetchApp.fetch(OPENAI_API_URL, options).getContentText()
);
return response;
} catch (e) {
Logger.log(e);
return null;
}
}
}
先ほどの src/main.ts
を少し修正します。
import { ChatGptClient } from "./chatgpt/client";
// ....
const checkGpt = () => {
const messages = [
{
role: "system",
content: "必ず関西弁で答えてください",
},
{ role: "user", content: "日本で最も高い山は?" },
];
// ChatGptClientを使うように修正
const gpt = new ChatGptClient();
const response = gpt.request(messages);
Logger.log(response?.choices[0].message.content);
};
これでデプロイして先ほどと同じように返答が返ってきていたらOKです。
次に src/main.ts
の checkHackerNews
に修正します。
const checkHackerNews = () => {
const client = new HackerNewsClient();
const topStories = client.topStories();
const parser = new Parser();
const content = parser.parse(topStories[0].url);
const gpt = new ChatGptClient();
const response = gpt.summarize(content);
Logger.log(response);
};
これでデプロイ、実行して要約がログに出力されていれば成功です!
※ urlによっては上手くparseできないurlもあります。その時は別のurlで試してみて下さい。
Slackに通知する
最後に要約した記事一覧をSlackにポストするようにしてみたいと思います。
slack通知用のclassを src/notifier/slack.ts
に作成します。
import URLFetchRequestOptions = GoogleAppsScript.URL_Fetch.URLFetchRequestOptions;
import HttpMethod = GoogleAppsScript.URL_Fetch.HttpMethod;
const SLACK_WEBHOOK_URL_KEY = "slack_webhook_url";
export interface ISlackPayload {
username?: string;
icon_emoji?: string;
icon_url?: string;
channel?: string;
text: string;
}
export class SlackNotifier {
constructor() {
this._webhookUrl = PropertiesService.getScriptProperties().getProperty(
SLACK_WEBHOOK_URL_KEY
);
}
private _webhookUrl: string;
public postChannel(payload: ISlackPayload): void {
const result = this.post(payload);
Logger.log(`result: ${result.getContentText()}`);
}
private post(
playload: ISlackPayload
): GoogleAppsScript.URL_Fetch.HTTPResponse {
const options: URLFetchRequestOptions = {
method: "POST" as HttpMethod,
payload: JSON.stringify(playload),
};
return UrlFetchApp.fetch(this._webhookUrl, options);
}
}
こちらも ChatGptClient
の時と同じ様にスクリプトプロパティにwebhookのurlを設定し、使う際にそこから取得して使っています。
最後に src/main.ts
の main
を以下に修正します。
const main = () => {
const client = new HackerNewsClient();
const topStories = client.topStories();
Logger.log(`Query hacker news top stories ${topStories.length} items`);
const parser = new Parser();
const gpt = new ChatGptClient();
const notifier = new SlackNotifier();
for (const story of topStories) {
const content = parser.parse(story.url);
const response = gpt.summarize(content);
// 必要に応じてchannel, username, icon_emoji等を設定
const playload: ISlackPayload = {
text: `Title: ${story.title}\nURL: ${story.url}\nSummary: ${response}`,
};
notifier.postChannel(playload);
}
};
デプロイして実行すると、Slackに通知が来れば成功です ✨
今回は通知のフォーマットに関しては何も考えていませんが、必要に応じて見やすいフォーマットに変更して試してもらえればと思います。
参考URL
Discussion