ChatGPT x Slackbotで議事録Bot作ってみた 

Slackbotを使用してslackのスレッド内で書き連ねたネタを回収し、ChatGPT APIを使用し良い具合に議論した内容をまとめてくれる議事録Botを作ってみました。

### Bot作成の背景

昨今爆速で普及しまくりのChatGPTに個人的に追いつけてないと危機感を抱いていたのと最近社内・社外でMTGをしながらSlackにメモを取る習慣がついてきており、メモを一手にまとめてくれる人いないかなーと思っていたところ、せや！チャットGPTに任せたろ！の精神で今回実装に至りました。

### 概要

アーキテクチャとしてはかなりシンプルな作りとなってます。

![slack_chatgpt_sequence_diagram.drawio (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481596/3d7a7852-1a62-5444-27e5-44cae012e4c0.png)



Slackであらかじめメンション（`@Slackbot名`）をトリガーにエンドポイントへPOSTを投げるbotを用意し、AWS Lambda経由でSlackAPI, ChatGPT APIと連携しSlackのスレッド内のメッセージを回収・分析し、最後にSlackに向けて議事録っぽいメッセージをプッシュする流れになってます。

### 事前準備

以下のアカウントが必要です

- Slackアカウント
- AWS アカウント
- Open AI アカウント、ChatGPT API

また、今回のbotを作成する場合、以下のAPIで課金要素が発生します。

- AWS (Lambda, API Gateway)
- ChatGPT API

### Open AI で支払い方法登録 ＆ ChatGPT API keyの取得

ChatGPT API を使用する場合課金が発生するため、先に支払い方法の登録を行う必要があります。

API key自体の取得は支払い方法登録せずとも取得できますが、支払い登録が済んで無い場合API呼び出しの際に429エラーを受け取り使用できなくなってます。

### 支払い方法登録

1. Open AIのアカウントを作成
2. ログイン後、画面右上のアカウントアイコンをクリック
3. Manage accountを選択
4. 画面左ナビゲーションペーンからBilling → Overviewを選択
5. Set up paid accountを選択
![image_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481596/b57ec2fe-5a7d-7bd2-fb58-339b826ea54d.png)
6. モーダルが出現するので、個人プロジェクトであれば`I’m an individual`を選択
![image_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481596/38f39c54-3775-75ed-cade-127ebd69e51e.png)
7. 支払い情報入力用のモーダルに切り替わるので入力後、`Set up payment method`を選択

![image_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481596/0ce89939-30ba-f38c-c51a-b97dc77ab207.png)
これで支払い登録完了です。

## ChatGPT API keyの取得

1. 同じく画面左のナビゲーションペーンからAPI Keysを選択
2. `+Create new secret key`を選択（初めてAPI keyを発行する場合はAPI key情報はなく、ボタンだけ表示されます。）
![image_4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481596/33df2956-6e36-5719-e91f-fd9d68849d4b.png)
3. API keyが発行されモーダルに表示されるのでコピーしてメモ帳などに貼り付け。
（※モーダルが閉じてしまうと2度と開けなくなります。コピーを忘れた場合はkeyを削除し、新たに発行する必要があります。）

これでChatGPT APIのkeyが取得できました。
## ChatGPT Slack BOT作成手順

大まかですが以下のような流れになります。

1. Slack内のslackbotがリクエストを送る先のLambdaとAPI Gatewayの作成
2. Slackbotの作成（Slackコンソール）
3. Slack API, ChatGPT APIを呼び出すLambdaの作成
4. １のLambdaと３のLambdaの繋ぎ込み

なんで４の作業がいるの？Lambda一つでできるでしょと思われそうですが、こちらはSlackの仕様上**6秒以内にレスポンスがない場合はタイムアウトとなってしまうため**その対策として応答用のLambda（手順１のLambda）とChatGPTにアクセスし結果をSlackへ通知するLambda（手順３のLambda）と2つ作成しております。

[こちら](https://qiita.com/kamata1729/items/fc6c31a3039599b48305)の記事が参考になりました。

## 1. LambdaとAPI Gatewayの作成

Slackbotだけ先に作成するでもいいんですが、bot作成後にエンドポイント入力の必要があるため先にLambdaを作成します。

私はServerless Frameworkを使用し、API Gatewayとセットで作成しましたが、下記を参考にAWSから直接作成されても問題ないかと思います。

Serverlessを使用せずにマニュアルで作る場合の参考記事
[https://qiita.com/miyuki_samitani/items/f01f1bd49334f97fe84c](https://qiita.com/miyuki_samitani/items/f01f1bd49334f97fe84c)

Serverless Frameworkを使用する場合
参考記事：[https://qiita.com/kousaku-maron/items/c591a1245bdd69c0dad3](https://qiita.com/kousaku-maron/items/c591a1245bdd69c0dad3)

今回はあくまでLambdaをデプロイするだけなので、記事内のProviderとFunctionsを設定すれば大丈夫です。
今回用意したServerless用のソースコードは下記の通りです。
### Serverless.yml

```jsx
// serverless.yml
service: slack-chatgpt
frameworkVersion: "3"

provider:
  name: aws
  runtime: nodejs18.x
  region: ap-northeast-1
  timeout: 30

plugins:
  - serverless-offline

functions:
  // Slackbotがリクエストを送るLambda
  slackinvoke:
    handler: slackInvoke.handler
    events:
      - httpApi: "*"
```

下記はSlackbotからアクセスを受けてレスポンスを返すLambdaです。
Post リクエストを受ける必要があるため、Expressを使用し、POSTリクエストを受けるとメッセージ "Hello from root!" を返すようにしています。
### slackinvoke.js

```jsx
const serverless = require("serverless-http");
const aws = require("aws-sdk");
const express = require("express");
const app = express();
const qs = require("querystring");

const lambda = new aws.Lambda({
  apiVersion: "2015-03-31",
  region: "ap-northeast-1",
});

app.post("/", async (req, res) => {
  const body = req.body;
  const urlCode = body.toString();
  const requestBody = qs.parse(urlCode);
  // slack botのメンション通知設定の際のVerification用
  if (JSON.parse(Object.keys(requestBody)[0]).type === "url_verification") {
    const parsedBody = JSON.parse(Object.keys(requestBody)[0]);
    return res.status(200).json({
      value: parsedBody.challenge,
    });
  }
  return res.status(200).json({
    message: "Hello from root!",
  });
});

module.exports.handler = serverless(app);
```
作成後はこんな感じでLambdaとAPI Gatewayが出来ているかと思います。

![image_5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481596/9e824894-f7ea-79cc-c596-42a9fbdd568e.png)

## 2. slackbotの作成

Labmdaを作成し、API Gatewayからエンドポイントを取得できたら次はSlackbotの作成です。

[こちら](https://qiita.com/landwarrior/items/dfac36a1aadac9ab0cd5)の記事を参考にし、Slack管理画面からSlackbotを作成しました。

スラッシュやイベントなどSlackbotを呼び出すトリガーは色々あったんですがスレッド内で呼べるものが欲しかったのでメンションイベントによる発火を選択しました。

数点気をつける点があるので記載。

#### 1. Slackメンション発火用のVerification用にLambda関数の改修

メンション発火(@[Slackbot名])の場合、Slack管理画面でイベントを選択した時点でverificationが行われるため、Lambda側で事前にSlackのVerificationを受け取りレスポンスを返すよう設定する必要があります。

![image_6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481596/82c3e5fd-829f-e360-7344-141f2a7f5721.png)

エンドポイント入力後に自動で下記のようなPayloadを持ったPOSTリクエストが送信されます。
応答ができれば上の画像のように`Verified`となりメンショントリガーでのSlackbot起動が登録できます。

#### Slackから送信されるPostリクエストのPayload
```jsx
// slack chat bot verification request body
{
    "token": "Jhj5dZrVaK7ZwHHjRyZWjbDl",
    "challenge": "3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P",
    "type": "url_verification"
}
```
上でお見せしたslackInvoke.jsの 下記の部分がVerificationの対応コードになります。

```jsx
// slackInvoke.js
...
  // slack botのメンション通知設定の際のVerification用
  if (JSON.parse(Object.keys(requestBody)[0]).type === "url_verification") {
    const parsedBody = JSON.parse(Object.keys(requestBody)[0]);
    return res.status(200).json({
      value: parsedBody.challenge,
    });
  }
...
```

#### 2. Bot Token Scopesの設定

 今回、Slackbotを使用しスレッド内の会話を読み取りChatGPTで議事録を作るため、スレッド内の会話を読み取るための権限設定がSlack側で必要になります。

1. [Slack api ページ](https://api.slack.com/)から`Your apps`を選択
2. 作成したアプリの選択
3. 左側のナビゲーションペーンからOAuth & Permissionsを選択
4. スクロールダウンするとScopesという項目が見えるのでそこで設定します

こちらはStep3の際に必要になります。

下記今回の実装で使用したScopeを記載してます。

**必要なScopes**

- app_mentioned:read
- channels:history
- channels:read
- chat:write
- groups:history
- im:history
- mpim:history

## 3. Slack API, ChatGPT APIを呼び出すLambdaの作成

色々と事前準備が多かったですが、いよいよコアの実装です。

とは言っても殆どAPI頼りで実際のところは１ファイルだけで事足ります。

```jsx
const serverless = require("serverless-http");
const { WebClient, LogLevel } = require("@slack/web-api");
const { Configuration, OpenAIApi } = require("openai");
// local test用
require("dotenv").config();

// ChatGPT APIインスタンス用Config
const configuration = new Configuration({
  apiKey: process.env.OPEN_AI_KEY,
});

// ChatGPT APIインスタンス
const openai = new OpenAIApi(configuration);

// SlackAPIインスタンス
const client = new WebClient(process.env.SLACK_TOKEN, {
  logLevel: LogLevel.DEBUG,
});

// slack apiを使用し特定のスレッドから会話を取得
const getAllMessagesInThread = async (slackEvent) => {
  const messages = await client.conversations.replies({
    channel: slackEvent.channel,
    ts: slackEvent.thread_ts,
  });

  // 会話をChatGPTが読みやすいように加工
  const conversations = messages.messages.reduce((acc, cur) => {
    return (acc += JSON.stringify({ user: cur.user, message: cur.text }));
  }, "");

  return conversations;
};

// ChatGPTへの質問関数
const askChatGPT = async (question, model = "gpt-3.5-turbo-0301") => {
  // 質問前の前提条件
  const prerequisite = `You're the minutes taker who summarize conversations happened while meeting.
  I'll give you conversations note, so I'd like you to create meeting minutes based on that conversation with rules.`;

  // 議事録作成のためのルール
  const rules = `These are the rules.
    - Meeting minutes must summarize with bullet point. You can ignore attendees and date in the meeting minutes.
    - Please write meeting minutes based on the language that uses the most in the conversation, but also prepare English translate version for foreigner too.
    - Please do not concise too much, we don't want to miss conversation itself because of summarize too much.
    - When you write Japanese meeting minutes, please do not use 丁寧語 like 〜しました at the end of each bullet point.
    - Please not include the message that only mention using @.
    `;

  // 質問実行関数
  const response = await openai.createChatCompletion({
    model: model,
    messages: [
      { role: "system", content: prerequisite },
      { role: "assistant", content: rules },
      { role: "user", content: question },
    ],
  });

  // 回答の抽出
  const answer = response.data.choices[0].message?.content;
  return answer;
};

// Lambda実行関数
async function publishMessage(event) {
  try {
    const parsedEvent = JSON.parse(Object.keys(event.apiGateway.event)[0]);
    const slackEvent = parsedEvent.event;

    const conversations = await getAllMessagesInThread(slackEvent);

    // ChatGPTへの命令文
    const question = `please make a meeting minutes based on below conversations.
    ${conversations}`;

    const answer = await askChatGPT(question);

    // slackメッセージプッシュ関数
    const result = await client.chat.postMessage({
      // The token you used to initialize your app
      token: process.env.SLACK_TOKEN,
      channel: process.env.CHANNEL_ID,
      text: answer,
      thread_ts: slackEvent.thread_ts,
    });
  } catch (error) {
    console.error(error);
  }
}

module.exports.handler = serverless(publishMessage);
```

大きく3つの関数に分かれています。

### getAllMessagesInThread

Slack apiを介してチャネル内の特定のスレッドの会話を取得する関数です。
レスポンスタイプがオブジェクト配列でメッセージ以外の要素も含んでいるため、ChatGPTが読みやすいように微妙に加工してます。

```jsx
// slack apiを使用し特定のスレッドから会話を取得
const getAllMessagesInThread = async (slackEvent) => {
  const messages = await client.conversations.replies({
    channel: slackEvent.channel,
    ts: slackEvent.thread_ts,
  });

  // 会話をChatGPTが読みやすいように加工
  const conversations = messages.messages.reduce((acc, cur) => {
    return (acc += JSON.stringify({ user: cur.user, message: cur.text }));
  }, "");

  return conversations;
};
```

### askChatGPT

ChatGPTへ質問を投げかける関数です。[参考記事](https://dev.classmethod.jp/articles/tried-hello-openai-api-with-nodejs-x-typescript/)をもとに作成してます。

今回、贅沢に日本語版だけでなく英語版も作ってくれと依頼してます。（どうなるかは不明）

以下議事録作成のためのルール

- 議事録を書いてください、出席者は無視してOK。
- 会話内で一番使われている言語をもとに議事録を書いてください、ただし、外国籍の方用に英語版も作ってください。
- 出来るだけ会話を逃したくないので、あまりまとめ過ぎないで下さい
- 日本語で議事録を書くときは 〜しました などの丁寧語は省いてください。
- ＠を用いたメンションは議事録に含めないで下さい。

どういう結果が得られるか分からないのでめっちゃお願いしてます。

```jsx
// ChatGPTへの質問関数
const askChatGPT = async (question, model = "gpt-3.5-turbo-0301") => {
  // 質問前の前提条件
  const prerequisite = `You're the minutes taker who summarize conversations happened while meeting.
  I'll give you conversations note, so I'd like you to create meeting minutes based on that conversation with rules.`;

  // 議事録作成のためのルール
  const rules = `These are the rules.
    - Meeting minutes must summarize with bullet point. You can ignore attendees and date in the meeting minutes.
    - Please write meeting minutes based on the language that uses the most in the conversation, but also prepare English translate version for foreigner too.
    - Please do not concise too much, we don't want to miss conversation itself because of summarize too much.
    - When you write Japanese meeting minutes, please do not use 丁寧語 like 〜しました at the end of each bullet point.
    - Please not include the message that only mention using @.
    `;

  // 質問実行関数
  const response = await openai.createChatCompletion({
    model: model,
    messages: [
      { role: "system", content: prerequisite },
      { role: "assistant", content: rules },
      { role: "user", content: question },
    ],
  });

  // 回答の抽出
  const answer = response.data.choices[0].message?.content;
  return answer;
};
```

### publishMessage

Lambda上での実行関数です。

上記2つの関数を内部で呼び出し、結果をSlackへ通知する関数になります。

```jsx
// Lambda実行関数
async function publishMessage(event) {
  try {
    const parsedEvent = JSON.parse(Object.keys(event.apiGateway.event)[0]);
    const slackEvent = parsedEvent.event;

    const conversations = await getAllMessagesInThread(slackEvent);

    // ChatGPTへの命令文
    const question = `please make a meeting minutes based on below conversations.
    ${conversations}`;

    const answer = await askChatGPT(question);

    // slackメッセージプッシュ関数
    const result = await client.chat.postMessage({
      // The token you used to initialize your app
      token: process.env.SLACK_TOKEN,
      channel: process.env.CHANNEL_ID,
      text: answer,
      thread_ts: slackEvent.thread_ts,
    });
  } catch (error) {
    console.error(error);
  }
}
```

後はServerless frameworkを使用しAWSへデプロイすればLambda反映完了です！

```jsx
serverless deploy
```

ではでは・・・お待ちかねのテスト、の前にもう一つ作業があります。

実装の前にAWS側でAPI 呼び出し用のLambdaと１で作成したSlackレスポンス用のLambdaを繋ぎこむ作業が必要です。

## lambdaの繋ぎ込み作業
1. AWS Lambdaからレスポンス用Lambdaを選択
2. 関数の概要から送信先を追加を選択
![image_7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481596/8c2fa788-8869-8a16-a47a-723da77f014b.png)

- ソース：非同期呼び出し
- 条件：正常
- 送信先タイプ：Lambda 関数

を選択し、先ほどデプロイしたAPI 呼び出し用の関数を選択

![image_8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481596/c61745a9-7b65-d695-2b65-0e3c7ad8c4eb.png)
**保存**を押し、下記のように設定されていれば繋ぎ込み完了です！
![image_9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481596/d3190cfd-ceb7-8224-36e1-cbf5fc35be61.png)

後は初めに作成したLambda(slackInvoke.js)に繋ぎ込み用の関数を挿入します。

```jsx
const invokeLambda = async (event) => {
  try {
    const params = {
      FunctionName: "slack-chatgpt-dev-slackchat",
      InvocationType: "Event",
      Payload: JSON.stringify({ ...event }),
    };
    await lambda.invoke(params).promise();
    return { statusCode: 200, body: "OK" };
  } catch (e) {
    console.log("error", e);
  }
};
```
挿入後は以下のようになります。
### slackInvoke.js
```jsx
const serverless = require("serverless-http");
const aws = require("aws-sdk");
const express = require("express");
const app = express();
const qs = require("querystring");

const lambda = new aws.Lambda({
  apiVersion: "2015-03-31",
  region: "ap-northeast-1",
});

const invokeLambda = async (event) => {
  try {
    const params = {
      FunctionName: "slack-chatgpt-dev-slackchat",
      InvocationType: "Event",
      Payload: JSON.stringify({ ...event }),
    };
    await lambda.invoke(params).promise();
    return { statusCode: 200, body: "OK" };
  } catch (e) {
    console.log("error", e);
  }
};

app.post("/", async (req, res) => {
  const body = req.body;
  const urlCode = body.toString();
  const requestBody = qs.parse(urlCode);
  if (JSON.parse(Object.keys(requestBody)[0]).type === "url_verification") {
    const parsedBody = JSON.parse(Object.keys(requestBody)[0]);
    return res.status(200).json({
      value: parsedBody.challenge,
    });
  }
  await invokeLambda(requestBody);
  return res.status(200).json({
    message: "Hello from root!",
  });
});

module.exports.handler = serverless(app);
```
### テスト

いよいよテストです。ChatGPTの性能やいかに・・・！！！

既にSlackbotがインストールされているワークスペースから作成したBotを使用したいチャネルに招待。

![image_10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481596/3976004b-31bb-0744-7340-6ea2abf6a151.png)
![image_11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481596/8fad1d67-5f0f-cd0e-ac61-7696f2cc096e.png)
招待完了後、スレッドを作成
![image_12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481596/7116275d-7be1-b62a-6136-4ded9b44acfd.png)
メッセージを適当に追加
![image_13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481596/49267fec-078f-9697-289a-a31e9eff4598.png)
botに議事録作成依頼
![image_14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481596/90d0622e-53b6-135e-d3ae-73eba329c40f.png)
来ました！！しかも英語付き・・・
![image_15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481596/3b2b96f1-6aea-4aa8-f61c-1c6cf668c1ab.png)


## Conclusion
Lambda経由でAPIを使用しているのでレスポンスは遅い（体感10−20秒くらい）ですが、
しっかりまとめて応答してくれました。（ChatGPTやばい・・・仕事取られるでござる・・・）
最後メンションが加えられてますが、これは多分私の聞き方（英語力）が悪い気がします。

実際のところ簡易的な議事録程度であればある程度まとめてくれてる印象です。
個人的には翻訳までやってくれるのはマジでありがたい。

正直セキュリティの面で社外とのミーティングでの使用は躊躇しますが（[Official](https://openai.com/policies/api-data-usage-policies)では一応提供された情報以外はプロダクト向上のために利用しないとは言ってはいますが・・・）、

議事録ではなく個人的なブレストとかでとにかく書きまくって最後によしなにまとめるにはうってつけなんじゃないかと思ってます。

今回は議事録用に作ってみましたが、実際はブレストのようなもっと用途があるような気がしておりポテンシャル高そうです！
ChatBotに限らず色んなプラットフォームで利用できそうなので用法用量を守って適度に楽できる程度に利用したいですね。笑

最後まで読んでいただきありがとうございました。
（**この文章は１００％自作です**）