OpenAI API + Slack Bolt なんちゃってChatGPTを作る! – 付録ソースコード

はじめに

元記事をご覧でない方は、そちらを先にご覧いただくことをお勧めします。

こちらは、Slack & ChatGPTのBotについてのソースコードになっています。諸事情によりGithubレポジトリを公開できないため、こちらでコードを掲示する方式とさせていただきます。

使用言語はTypeScriptです。

今回の開発は同期の 内山 くんと共同で行ったため、記事執筆も二人で行っております。

目次

  1. 全体のまとめ
  2. ルートディレクトリ(/)
  3. srcディレクトリ(src/)

全体のまとめ

ルートからtreeコマンドを行った結果はこのようになっています。 各種ファイルに関して内容をコードブロックで記述していきます。

.
├── .env.sample
├── .gitignore
├── README.md
├── docker-compose.yml
├── init.sh
├── package.json
├── src
│   ├── app.ts
│   ├── index.ts
│   ├── types
│   │   └── index.d.ts
│   └── utils
│       ├── ai.ts
│       └── index.ts
└── tsconfig.json

ルートディレクトリ直下、srcディレクトリ内の二つに分けて掲示します。

ルートディレクトリ

package.json

ランタイムがNode.jsなのでpackage.jsonがあります。

{
  "name": "aionslackthreads",
  "version": "1.0.0",
  "description": "User friendly Slack bot for using OpenAI GPT.",
  "main": "index.js",
  "scripts": {
    "start": "tsc && TS_NODE_BASEURL=./dist node -r tsconfig-paths/register dist/index.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@slack/bolt": "^3.13.1",
    "@slack/web-api": "^6.8.1",
    "@types/node": "^20.1.5",
    "dotenv": "^16.0.3",
    "openai": "^3.2.1",
    "typescript": "^5.0.4"
  },
  "devDependencies": {
    "ts-node": "^10.9.1",
    "tsconfig-paths": "^4.2.0"
  }
}

tsconfig.json

TypeScriptを用いたので、tsconfig.jsonがあります。pathエイリアスを利用しています。

{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "outDir": "dist",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "baseUrl": "./src/",
    "paths": {
      "@api/*": [
        "api/*"
      ],
      "@components/*": [
        "components/*"
      ],
      "@layouts/*": [
        "layouts/*"
      ],
      "@pages/*": [
        "pages/*"
      ],
      "@repositories/*": [
        "repositories/*"
      ],
      "@utils/*": [
        "utils/*"
      ]
    },
    "typeRoots": [
      "types",
      "node_modules/@types"
    ]
  },
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "node_modules",
    "dist"
  ]
}

init.sh

Dockerでのデプロイに関して、npm installを自動で行わせるためのShellScriptです。

#!/bin/sh

if [ -d "./node_modules" ]; then
  echo "node_modules is exit"
else
  npm install
fi

docker-compose.yml

Docker composeを使うための設定ファイルです。 こちらは、デプロイ環境がDockerだったため作成しました。

version: '3'
services:
  app:
    image: node:20
    restart: always
    environment:
      - DEBUG=app:*
    env_file: ./.env
    tty: true
    ports:
      - '3001:3000'
    volumes:
      - ./:/app
    working_dir: /app
    command: >
      bash -c "sh init.sh && npm start"

.gitignore

gitにモジュールやdistを上げないための設定です。

node_modules
.env
dist/

.env.sample

.envファイルで環境変数を渡しているので、.envファイルを作成する必要があるのですが、それのサンプルとなります。こちらを参考に各値を設定するとプログラムに正しく認識されます。

OPENAI_API_KEY=
SLACK_SIGNING_SECRET=
SLACK_BOT_TOKEN=
CLIENT_ID=
APP_ID=

srcディレクトリ

index.ts

メインエントリです。

import { GenericMessageEvent } from '@slack/bolt';
import { Message } from '@slack/web-api/dist/response/ConversationsRepliesResponse';
import { app } from 'app';
import { getThread, isMentioned } from '@utils/index';
import { getAnswer } from '@utils/ai';

//アプリが起動時に呼ばれるメソッド
(async () => {
  await app.start(3000);
  console.log(`⚡️ Bolt app is running!`);
})();

// メンションに反応
app.message(/.*/, async ({ message, context, client }) => {
  const botUserId: string | undefined = context.botUserId;
  const botId: string | undefined = context.botId;
  if (botUserId === undefined) return;
  if (botId === undefined) return;

  const msg: GenericMessageEvent = message as GenericMessageEvent;
  if (msg.text === undefined) return;

  // メンションされていない場合は反応しない
  if (!msg.text.includes(`<@${botUserId}>`)) return;

  // スレッドには反応しない
  if (msg.thread_ts !== undefined) return;

  const response = await client.chat.postMessage({
    channel: msg.channel,
    thread_ts: msg.event_ts,
    text: '入力中...'
  });
  const TypingTs: string | undefined = response.ts;
  if (TypingTs === undefined) return;

  const threadContext: ThreadContext = {
    channel_id: msg.channel,
    thread_ts: msg.event_ts
  }
  const thread: Message[] | undefined = await getThread(threadContext);
  const botInfo: BotInfo = {
    botUserId: botUserId,
    botId: botId
  }
  if (thread === undefined) return;
  const answer: string = await getAnswer(botInfo, thread);

  await client.chat.update({
    channel: msg.channel,
    thread_ts: msg.event_ts,
    ts: TypingTs,
    text: answer
  });
});

// スレッドに反応
app.message(/.*/, async ({ message, context, client }) => {
  const botUserId: string | undefined = context.botUserId;
  const botId: string | undefined = context.botId;
  if (botUserId === undefined) return;
  if (botId === undefined) return;

  const msg: GenericMessageEvent = message as GenericMessageEvent;

  // スレッド以外には反応しない
  if (msg.thread_ts === undefined) return;

  // botにも反応しない
  if (msg.bot_id !== undefined) return;

  const threadContext: ThreadContext = {
    channel_id: msg.channel,
    thread_ts: msg.thread_ts
  }
  const thread: Message[] | undefined = await getThread(threadContext);
  if (thread === undefined) return;

  // メンションされていない場合は反応しない
  if (!isMentioned(botUserId, thread)) return;

  const response = await client.chat.postMessage({
    channel: msg.channel,
    thread_ts: msg.thread_ts,
    text: '入力中...'
  });
  const TypingTs: string | undefined = response.ts;
  if (TypingTs === undefined) return;

  const botInfo: BotInfo = {
    botUserId: botUserId,
    botId: botId
  }

  const answer: string = await getAnswer(botInfo, thread);

  await client.chat.update({
    channel: msg.channel,
    thread_ts: msg.thread_ts,
    ts: TypingTs,
    text: answer
  });
});

app.ts

import { App } from '@slack/bolt';
import dotenv from 'dotenv';
dotenv.config();

export const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET
});

utils/ai.ts

OpenAIのAPIとの通信についての関数です。

import { ChatCompletionRequestMessage, Configuration, OpenAIApi } from 'openai';
import { Message } from '@slack/web-api/dist/response/ConversationsRepliesResponse';

export const getAnswer = async (botInfo: BotInfo, thread: Message[]) => {
  const chatLog: ChatCompletionRequestMessage[] = [];
  thread.forEach((msg) => {
    const isBot = msg.bot_id !== undefined;
    if (isBot && msg.user !== botInfo.botUserId) return;
    chatLog.push({
      role: isBot ? 'assistant' : 'user',
      content: msg.text || '',
    });
  });
  try {
    //OpenAIに接続する。
    const openai = new OpenAIApi(
      new Configuration({ apiKey: process.env.OPENAI_API_KEY })
    );
    const completion = await openai.createChatCompletion({
      model: 'gpt-3.5-turbo',
      messages: chatLog,
    });
    if (completion.data.choices[0].message === undefined) {
      throw new Error('No response from AI.');
    }
    const answer = completion.data.choices[0].message.content;
    return answer;
  } catch (error) {
    console.log(error);
    return 'エラーが発生しました。';
  }
};

utils/index.ts

Slack関連の関数のまとめです。

/**
 * @description slack/boltで利用する関数まとめ。とりわけSlackからのオブジェクトの処理を記述する。
 */
import { Message } from '@slack/web-api/dist/response/ConversationsRepliesResponse';
import { app } from 'app';

export const getThread = async (threadContext: ThreadContext) => {
  const result = await app.client.conversations.replies({
    channel: threadContext.channel_id,
    ts: threadContext.thread_ts,
  });
  return result.messages;
};

export const isMentioned = (botUserId: string, messages: Message[]) => {
  let ifMentioned = false;
  const mention = `<@${botUserId}>`;
  messages.forEach((msg) => {
    if (!msg.text) return;
    if (msg.text.includes(mention)) {
      ifMentioned = true;
    }
  });
  return ifMentioned;
};

types/index.d.ts

簡単のため、型はdeclareでプロジェクト全体に定義しました。

declare type ThreadContext = {
  channel_id: string;
  thread_ts: string;
};

declare type BotInfo = {
  botUserId: string,
  botId: string
}

操作方法など

README.md

User friendly Slack bot for using OpenAI GPT.


## What is this?

Slackを通じてGPTを利用できる社内ツールとして開発しています。

登録したチャンネル内でメンションしてメッセージを送信すると、そのスレッド内で対話形式でGPTを利用できます。

## How to use?

`.env.sample`を参考に`.env`を作成してください。

作成したら、あとはコンテナを起動するだけです。


docker compose up


3001番が使われている場合は適宜`docker-compose.yml`を編集してください。

`package.json`に更新があった場合は`node_modules`を削除してからコンテナを起動してください。`init.sh`のパーミッションエラーが出たら適宜変更してください。

## Note

このアプリケーションは、`https://localhost:3001` を、グローバルからアクセスできるようにする必要があります。(`docker-compose.yml`の設定により、ポートは異なります。)
Apache,Nginxなどのミドルウェアでリバースプロキシして利用するか、Cloudflare Zero Trustのトンネルなどを利用して、グローバルアドレスからアクセス可能にし、Slack APIの、APP Event Subscriptions Request URLに設定します。
それにより、Slack APIにbotがアクセス可能となります。