5分で作るGPT-4oと会話できるSlackBOT
GPT-4o を社内の Slack でアシスタント的に使いたく BOT を実装したので作り方を解説します
ソースコードとかわからなくてもいいから作りたいという方からどう実装したらいいのか知りたい方まで読んでいただけるよう「爆速構築」パートと「解説」パートに分かれています
この SlackBOT の仕様
- メンションされるとそのスレッドに返答してくれます
- メンションを受け取った時点で 👀 のリアクションを付けます
- スレッドの内容はコンテキストとして認識します
- スレッド内の添付された画像もすべてコンテキストとして認識します
スレッドの途中でメンションしても反応してくれるようにしました
爆速構築
1. Glitch で Remix
以下を踏むだけ
2. Slack アプリを作る
こちらにアクセスして
マニフェストで一発作成
{Glitchプロジェクト名}
は Remix したプロジェクト名に書き換えてください
display_information:
name: GPT-4o
description: GPT-4oと会話してみよう
background_color: "#2c2d30"
features:
bot_user:
display_name: GPT-4o
always_online: false
oauth_config:
scopes:
bot:
- app_mentions:read
- channels:history
- chat:write
- files:read
- reactions:write
settings:
event_subscriptions:
request_url: https://{Glitchプロジェクト名}.glitch.me/slack/events
bot_events:
- app_mention
org_deploy_enabled: false
socket_mode_enabled: false
token_rotation_enabled: false
3. Slack アプリをインストール
作成直後の画面にインストールと促されるのでいわれるがままにインストールしてください
見失ったら Settings セクションの Install App から実行できます
4. Glitch の環境変数を設定
4 つあります
変数名 | 値の取り方 |
---|---|
SLACK_SIGNING_SECRET | Settings の Basic Information の App Credentials の Signing Secret |
SLACK_BOT_TOKEN | Features の OAuth & Permissions の Bot User OAuth Token |
SLACK_BOT_USER_ID | Slack のアプリページの詳細のメンバー ID(画像参照) |
OPENAI_API_KEY | OpenAI API コンソールで発行 |
5. API の検証
Slack アプリの Features の Event Subscriptions で検証を実施
完成。
解説
使用しているライブラリ
ライブラリ | 用途 |
---|---|
@slack/bolt | Slack のイベントを受け取るため |
@tryfabric/mack | Markdown を Slack Message Blocks に変換するため |
axios | OpenAI API を叩くため |
express | ファイルを OpenAI API に提供するため |
Bolt の基本構造
アプリインスタンスを生成して、そのインスタンスにイベントを登録していくスタイルです
const { App, ExpressReceiver } = require("@slack/bolt");
const app = new App({
signingSecret: process.env.SLACK_SIGNING_SECRET,
token: process.env.SLACK_BOT_TOKEN,
receiver: new ExpressReceiver({
signingSecret: process.env.SLACK_SIGNING_SECRET,
customPropertiesExtractor: ({ headers }) => ({ headers }),
}),
});
app.use(async ({ payload, context, next }) => {
// Slackからのリクエストのバリデーション
});
app.event("app_mention", async ({ event, client }) => {
// 生成した回答をSlackのスレに返す
});
(async () => {
await app.start(process.env.PORT || 3000);
console.log("⚡️ Bolt app is running!");
})();
リクエストのバリデーション
Slack からのリクエストのうち編集時に飛んでくるリクエストとリトライ時に飛んでくるリクエストは無視するようにします
app.use(async ({ payload, context, next }) => {
+ if (payload.edited) return;
+ if (context.headers["x-slack-retry-num"]) return;
+ await next();
});
とりあえずリアクションを返す
app.event("app_mention", async ({ event, client }) => {
+ // とりあえずリアクションを返す
+ client.reactions.add({
+ channel: event.channel,
+ timestamp: event.ts,
+ name: "eyes",
+ });
});
Bolt は簡単ですね
スレッドの内容を取得する
スレッドの内容を取得するためにはconversations.replies
を使います
app.event("app_mention", async ({ event, client }) => {
client.reactions.add({});
+ // スレの投稿を取得する
+ const messages = [];
+ let next_cursor;
+ if (event.thread_ts) {
+ do {
+ const replies = await client.conversations.replies({
+ channel: event.channel,
+ ts: event.thread_ts,
+ cursor: next_cursor,
+ });
+ messages.push(...replies.messages.filter((e) => e.ts < event.ts));
+ next_cursor = replies.response_metadata.next_cursor;
+ } while (next_cursor);
+ }
});
Open API 形式のリクエストへのコンバーターを実装
OpenAI API にリクエストを送るためには、リクエストの形式を変換する必要があります
メンション部分は OpenAI からの返答でUxxxxさん
となる時があるので削除しています
await ないですが画像を処理する時のために async にしています
+const convertMessageToOpenAIFormat = async (message) => {
+ const text = message.text.replace(/<@U.{10}>/g, "").trim();
+ return {
+ role: message.user === process.env.SLACK_BOT_USER_ID ? "assistant" : "user",
+ content: [
+ ...(text ? [{ type: "text", text }] : []),
+ ],
+ };
+};
OpenAI API にリクエストを送る
system の role に Slack の BOT であることを示すメッセージを追加しています
ここを変えると癖のある BOT になるかもしれません
+const axios = require("axios");
app.event("app_mention", async ({ event, client }) => {
client.reactions.add({});
const messages = [];
let next_cursor;
if (event.thread_ts) {}
+ // OpenAI APIで回答を生成する
+ const response = await axios.request({
+ method: "post",
+ url: "https://api.openai.com/v1/chat/completions",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
+ },
+ data: {
+ model: "gpt-4o",
+ messages: [
+ {
+ role: "system",
+ content: [
+ {
+ type: "text",
+ text: "あなたはSlackのチャットボットです。",
+ },
+ ],
+ },
+ ...(
+ await Promise.all(
+ [...messages, event].map(convertMessageToOpenAIFormat)
+ )
+ ).filter((e) => e.content.length),
+ ],
+ },
+ });
});
Slack に回答を返す
app.event("app_mention", async ({ event, client }) => {
client.reactions.add({});
const messages = [];
let next_cursor;
if (event.thread_ts) {}
const response = await axios.request({});
+ // 生成した回答をSlackのスレに返す
+ const text = response.data.choices[0].message.content;
+ await client.chat.postMessage({
+ channel: event.channel,
+ thread_ts: event.ts,
+ text,
+ });
});
ほんと Bolt は簡単ですね
一応ここまででテキストだけのやり取りはできるようになっています
Slack に BlockKit を使ったメッセージを返す
GPT-4o からしばしば Markdown 形式で回答が来るので BlockKit に変換して返すようにします
Slack は mrkdwn なので、そのまま返すと一部見にくくなってしまいます
+const { markdownToBlocks } = require("@tryfabric/mack");
app.event("app_mention", async ({ event, client }) => {
client.reactions.add({});
const messages = [];
let next_cursor;
if (event.thread_ts) {}
const response = await axios.request({});
// 生成した回答をSlackのスレに返す
const text = response.data.choices[0].message.content;
await client.chat.postMessage({
channel: event.channel,
thread_ts: event.ts,
text,
+ blocks: await markdownToBlocks(text),
});
});
ファイルサーバーを立てる
Slack Bolt ではファイルサーバーにはできないので、Express を使って画像を提供します
一時的に保存するディレクトリを作成して、そのディレクトリに画像を保存して提供します
+const fs = require("fs");
+const path = require("path");
const { App, ExpressReceiver } = require("@slack/bolt");
const axios = require("axios");
+const express = require("express");
+const fileDirectory = "/tmp/files";
const convertMessageToOpenAIFormat = async (message) => {};
const app = new App({});
+const expressApp = express();
+fs.mkdir(fileDirectory, { recursive: true }, (err) => {
+ if (err) throw err;
+});
+expressApp.get("/files/:filename", (req, res) => {
+ res.sendFile(path.join(fileDirectory, req.params.filename), (err) => {
+ if (err) res.status(404).send("File not found");
+ });
+});
+app.receiver.app.use(expressApp);
app.use(async ({ payload, context, next }) => {});
app.event("app_mention", async ({ event, client }) => {});
(async () => {})();
Slack からファイルをダウンロードする
適当なファイル名に変換してダウンロード可能な URL にします
const convertMessageToOpenAIFormat = async (message) => {
const text = message.text.replace(/<@U.{10}>/g, "").trim();
return {
role: message.user === process.env.SLACK_BOT_USER_ID ? "assistant" : "user",
content: [
...(text ? [{ type: "text", text }] : []),
+ ...(await Promise.all(
+ (message.files ?? []).map(async (file) => {
+ const filename = `${Date.now()}-${file.url_private_download
+ .split("/")
+ .at(-1)}`;
+ const response = await axios.get(file.url_private_download, {
+ headers: {
+ Authorization: `Bearer ${process.env.SLACK_BOT_TOKEN}`,
+ },
+ responseType: "stream",
+ });
+ response.data.pipe(
+ fs.createWriteStream(path.join(fileDirectory, filename))
+ );
+ return {
+ type: "image_url",
+ image_url: {
+ url: `https://${process.env.PROJECT_NAME}.glitch.me/files/${filename}`,
+ },
+ };
+ })
+ )),
],
};
};
OpenAI API が受け付けているファイル形式かチェックしていないのでダメなやつが添付されるとエラーになりそうですが、ヨシ!
(OpenAI の仕様変更を気にしたくなかっただけです)
Discussion
GPT-4oからの返答をBlockKitに変換する処理を追加しました