🔖

Slack上でChatGPTと会話できるようにした

2023/03/07に公開

ふとSlackのスレッド形式でChatGPTの会話ができるといいんじゃないかと思ってSlack Botを作ってみました。

やっていることはすごく単純で、スレッドの会話を拾ってそれをChatGPTに投げ、再びスレッド内のメッセージとして投稿するということをやっています。

今回はAWS Lambdaのnode.js18上で構築しました。

const { App, AwsLambdaReceiver } = require("@slack/bolt");

// Boltの初期化
const awsLambdaReceiver = new AwsLambdaReceiver({
  signingSecret: process.env.SLACK_SIGNING_SECRET,
});
const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  receiver: awsLambdaReceiver
});

app.event('app_mention', async ({ client, context, event, say }) => {
  if (context.retryNum) {
    // 3秒以内に終わらないのでリトライが来る。リトライはそのまま終了させる

  } else {
    /*
    1. メンションとして届くので先頭のメンションを消す
    2. スレッドとして返信するために、届いたメッセージがスレッド内かどうか確認する
    3. スレッドなら全てのスレッドを取得して、ユーザーとBOTの会話のみに絞る
    4. メッセージを整形して、OpenAIに投げる
    5. OpenAIからのレスポンスをスレッド形式で投稿する
    */

    // 頭のメンションを消す
    const text = event.text.replace(/^<@U(\d|\w)+>/g, "").trim();
    // スレッド内のメッセージならthread_tsがあるのでそれにpostすればスレッドが続く、なければこのメッセージをスレッドの先頭にする
    const threadTs = event.thread_ts ?? event.ts;
    
    const sayRes = await say({ thread_ts: threadTs, text: `かんがえちゅう... :thinking_face:` });
  
    try {
      // get all replies
      let messages = [];
      if (event.thread_ts) {
        const replies = await client.conversations.replies({
          channel: event.channel,
          ts: event.thread_ts
        });
        const discussions = replies.messages?.filter(m => m.user === process.env.BOT_USER || m.text?.includes(process.env.BOT_USER));
        messages = discussions?.map(d => {
          const user = d.user === process.env.BOT_USER ? 'assistant' : 'user';
          return { role: user, content: d.text};
        });
      }
      // add user message
      messages.push({ role: 'user', content: text });
      const answer = await question(messages);
      
      // delete thinking message
      if (sayRes.ts) {
        await client.chat.delete({channel: event.channel, ts: sayRes.ts});
      }
      
      await say({ thread_ts: threadTs, text: answer });
    } catch(e) {
      await say(e.message);
    }
  }
});

exports.handler = async(event, context, callback) => {
  const handler = await awsLambdaReceiver.start();
  return handler(event, context, callback);
};

async function question(messages) {
  const headers = {
    'Authorization': 'Bearer ' + process.env.OPENAI_APIKEY,
    'Content-Type': 'application/json'
  };
  const req = {
      model: 'gpt-3.5-turbo',
      messages: messages
  };
  
  /* global fetch */
  const res = await fetch('https://api.openai.com/v1/chat/completions', {
    method: 'post',
    headers: headers,
    body: JSON.stringify(req)
  });
  const resBody = await res.json();
  console.log(JSON.stringify(resBody));
  return resBody.choices[0].message.content;
}

気を付けるポイントとして、Lambdaのnode.js16からはデフォルトでファイル名がindex.mjsになっていることです。これだとESModule形式となるのですが、slack/boltライブラリがcommonjsのためインポートがうまくいきません。なのでファイル名をindex.jsと拡張子を変更してあげるとcommonjs形式になります。

またBOTのレスポンスとして基本3秒以内が求められますが、今回はChatGPTにリクエストする都合上どうしても間に合いません。その場合Slackからリトライがまた届きます。これを受け付けてしまうと1回のメンションで複数返事を返してしまいます。リトライの場合はcontext.retryNumにリトライ数がつくのでこれがある場合はリトライのリクエストと判定しスルーするようにしています。

あとはChatGPTのレスポンスが来る間は考え中と表示し、結果が返ってきたらそれを削除して返事を再度投稿しています。

実際にスレッド形式で会話してみたのがこちらです。

謎のうさぎ推しだったり途中で間違ったりしていますが、軌道修正して最後にはしりとりを終えているのでしっかりと前の文脈は引き継いでいますね!

OpenAI API経由で利用する場合はそのログを学習には利用しない、ただ悪用監視のために30日は保持するとなっています。
これにより社内データを送ってそれが学習されるというリスクがなくなったので、社内のコードを投げても大丈夫そうに見えます。

https://platform.openai.com/docs/data-usage-policies

OpenAI will not use data submitted by customers via our API to train or improve our models, unless you explicitly decide to share your data with us for this purpose. You can opt-in to share data.
Any data sent through the API will be retained for abuse and misuse monitoring purposes for a maximum of 30 days, after which it will be deleted (unless otherwise required by law).

Discussion