SlackのThreadを生成AIで要約してくれるSlackBot

## はじめに
かねてから生成AIを使って何か面白いことできないかなー、と思ってたところ
生成AIの勉強会を聞いていたらこのアイデアを閃いたので実装してみました。
あまりコードは参考にならないかもですが、生成AIの活用事例として読んでもらえると幸いです。
## 事前準備
1. SlackAppの準備
    カスタムSlackAppからslack-eventを受信できる状態になっている前提です。
    ここら辺は公式ドキュメントや良記事があるので、そちらでどうぞ。
    なお、本記事ではNodejs + bolt-js with TypeScriptで受ける前提でサンプルコードを書いています。
2. 生成AIのAPIの準備
    本記事では生成AIのAPIについて自体は扱いませんのでお好きなAPIを用意してください。
    ※[注意] 情報等の漏洩対策の為、ビジネス利用をする場合privateなAPIの利用をお勧めします。

## 雑設計
まずは雑に流れを考えてみます。
- ユーザがスレッドの末尾で 「@bot さん、このスレッドを要約して」的なポストをトリガーに
- イベントを受け取ったバックエンドでスレッドの中身を読み取って、要約してもらえるPromptに加工して、生成AIのAPIに投げる
- レスポンスの内容をメンションをもらったスレッドにポストする

簡単ですね♪

## プロンプトを考える
あまり専門的な知識はないので、フィーリングで考えます。
生成AIのコンソールを使って多少試行錯誤しつつ、一旦は決めで、あとで実際のスレッドで試してみてチューニングしていきます。
ひとまず下記な感じで落ち着きました。

<入力>
```
以下の会話を簡潔に要約して最後に結論を書いて
Aさん「今日はいい天気ですね」
Bさん「そうですね。こんな日はピクニックに行きたいですね」
Aさん「それはいい提案ですね、ぜひ行きましょう」
```
<出力>
```
AさんとBさんは、今日の良い天気について話し合い、ピクニックに行くことを提案して同意しました。
```

Slackは会話なので、会話調にするのがいいかなって思い、`${user}さん 「${message}」`的なフォーマットにすることにしました。
（正解かは分かりません）

## 実装
さてそれでは実際に実装してみます。

1. app_mentionイベントのhandlerを追加
    まずはメンションを受け取るところ。
    本題じゃないので色々端折ってますが、SlackのSDKである[bolt-js](https://slack.dev/bolt-js/concepts#event-listening)をつかっています。
    ```ts
    app.event('app_mention', async ({ event, say }) => {
        await handleMention(event, say, app);    
    });
    ```


1. post内容を判定して要約ロジックに飛ばす
    今回は「要約して」というキーワードをトリガーにします。
    ```ts
    const handleMention = async (
        event: AppMentionEvent,
        say: SayFn,
        app: App
    ) => {
        // event.thread_ts がない場合、スレッドではないので無視します。 
        if (event.text.includes('要約して') && event.thread_ts) {
            await summeriseThread(app, event.channel, event.thread_ts, event.ts, event.user, sayInThread);
        }
    }
    ```
1. 処理本体を記述
    スレッドの会話を取得して加工して生成AIのAPIに渡して結果をSlackにPostします。
    スレッドの会話履歴は[conversations.replies API](https://api.slack.com/methods/conversations.replies)で取得します。対象のSlackAppの権限に`channels:history`の追加が必要になりますのでご注意ください。
    ```ts
    const = PROMPT_HEADER = '以下の会話をとても簡潔に要約して最後に結論を書いてください。';
    
    export const summeriseThread = async (app: App, channel: string, thread_ts: string, ts: string, user: string) => {
        // まずは、メッセージを受け取ったことを伝える
        const mySayRes =await sayInThread('生成AIを使って、このスレッドの要約をしますね。少々お待ちください。', thrad_ts);

        // 対象のスレッドの全replyをconversations.replies APIから取得
        const replies = await app.client.conversations.replies({
            channel: channel,
            ts: thread_ts,
        });

        const messages = replies.messages?
            // 「要約して」というpostと「要約しますね」というpostを除外
            .filter(message => message.ts !== ts && message.ts !== mySayRes.ts)
            // replyを`user 「メッセージ」`の形に変換
            // <@USER_ID> にしているのは、ユーザ名をそのままメンションの形にする為
            .map(message => `<@${message.user}>さん 「${message.text}」`).join("\n");

        if (messages) {
          // 生成AIのAPIに渡して結果をもらう
          const response = await sendMessageToGenerativeAi(PROMPT_HEADER + '\n' + messages);
          // 結果をそのままSlackの対象ThreadにPost
          const p1 = sayInThread(response, thread_ts);
          // チューニング用にログをとっておく（中身見る事はユーザに伝えておきます）
          const p2 = putSummriseLog(channel, thread_ts, user, name || '', response);
          await Promise.all([p1, p2]);
        } else {
          await sayInThread('要約する対象のメッセージがありませんでしたよ。', thread_ts);
        }
    }
    ```

## 試してみる
こんな感じ。
![summery-example.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77104/c2854f4a-7e46-39b7-38e4-fc27332c5fa1.png)


## やってみて思ったこと
ひとまず形にするのは思いのほか簡単でした（数時間程度）。すごい時代になりましたね。
ただ、常に妥当な結果が出力されるようにするには、道のりは遠いな、という印象です。
プロンプトエンジニアリングは全くのド素人なので、どこかで一度ちゃんと勉強してみたい気持ちになりました。