#59　OpenAIのAPIを使用してアシスタントをDiscord上に作成してみる

# 概要
OpenAIのAPIを使用して個人用のアシスタントをDiscord上に作成してみます

## 事前準備
1. [Discord Developer Portal](https://discord.com/developers/applications/)からApplicationを作成して、トークンを取得します
2. [OpenAI API](https://platform.openai.com/api-keys)からシークレットキーを作成します
3. .envファイルに以下のように記載します
    ```
    DISCORD_TOKEN="1で取得したトークン"
    OPEN_AI_TOKEN="2で取得したシークレットキー"
    ```
4. 必要なライブラリをインストールします
    ```
    $ npm install discord.js dotenv log4js openai
    ```

# 受け取ったメッセージをそのまま返す
## 実装
受け取ったメッセージをそのまま返すコードを書いてみます

ここでのポイントは以下の3点です
- client.login()でログインを行います。トークンを引数にとります
- client.on('messageCreate', async message => {})でメッセージの受信を行います
- message.reply()でメッセージの返信を行います。引数はいくつかの形式で渡せますが、今回は文字列を渡します
```TypeScript
import { Client } from "discord.js";
import 'dotenv/config'

const client = new Client({
    intents: [
        GatewayIntentBits.GuildMessages, //メッセージを取得できる
        GatewayIntentBits.MessageContent, //メッセージの内容を取得できる
        GatewayIntentBits.GuildWebhooks, //Webhook関係を参照・設定できる
        GatewayIntentBits.Guilds, 
    ]
});

client.on('messageCreate', async message => {
    //送信者がbotならスルー
    if (message.author.bot) {
        logger.debug('message.author.bot == true');
        return;
    }

    //受信したメッセージを返信
    message.reply(message.content);
});

//ログイン
client.login(process.env.DISCORD_TOKEN);
```

## 動作確認
以下のコマンドを実行し、Discord上でメッセージを送信してみると、画像のように返信することを確認します
```
$ npx ts-node ./src/main.ts
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843084/1140f904-de6c-ac99-6629-c5bb22ae19c9.png)


# メッセージ履歴の取得
## 実装
チャット形式での会話にはメッセージ履歴の取得が必要ですので、メッセージの履歴を取得して返信するコードを書いてみます

ここでのポイントは以下の1点です
- channel.messages.fetch()でチャンネルのメッセージ履歴を取得します
```TypeScript
client.on('messageCreate', async message => {
    //送信者がbotならスルー
    if (message.author.bot) {
        logger.debug('message.author.bot == true');
        return;
    }
    const channel = message.channel;

    //メッセージを取得して古い順に並び変え
    const messagesCollection = await channel.messages.fetch();
    messagesCollection.sort((messageA, messageB) => messageA.createdTimestamp - messageB.createdTimestamp);

    const messages: string[] = [];
    for (let message of messagesCollection) {
        messages.push(message[1].content);
    }
    logger.debug(message);

    //返信
    message.reply(JSON.stringify(messages));
});
```

## 動作確認
先ほどと同様に以下のコマンドを実行し、Discord上でメッセージを送信すると、画像のようにメッセージ履歴の配列を返信してくれることを確認します
```
$ npx ts-node ./src/main.ts
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843084/4721b99f-4b62-cd46-0d06-eca83ab0f296.png)

# OpenAIのAPIを呼び出す
## 実装
先ほど取得したメッセージ履歴を用いて、OpenAIのAPIを呼び出して会話できるコードを書いてみます

ここでのポイントは以下の4点です
- message.author.idで送信者のユーザIDを取得します
- client.user.idで自身のユーザIDを取得します
- openai.chat.completions.create()でAPIを呼び出しメッセージを生成します
- ChatCompletion.choices[0].message.contentで生成された文字列を抽出します(通常ではchoicesは0番目を取得します)
```TypeScript
import OpenAI from 'openai';
import { ChatCompletionMessageParam } from "openai/resources";
const openai = new OpenAI({
    apiKey: process.env.OPEN_AI_TOKEN,
});

client.on('messageCreate', async message => {
    //送信者がbotならスルー
    if (message.author.bot) {
        logger.debug('message.author.bot == true');
        return;
    }
    const channel = message.channel;

    //メッセージを取得して古い順に並び変え
    const messagesCollection = await channel.messages.fetch();
    messagesCollection.sort((messageA, messageB) => messageA.createdTimestamp - messageB.createdTimestamp);

    //メッセージをOpenAIのAPIに渡せる形に成型する
    const messages: ChatCompletionMessageParam[] = [];
    for (let message of messagesCollection) {
        const role = ((message[1].author.id == client.user?.id) ? "assistant" : "user");
        messages.push(
            {
                role: role,
                content: message[1].content
            }
        );
    }
    logger.debug(message);

    //APIにメッセージを投げて返答を抽出
    const content = (await openai.chat.completions.create({
        messages,
        model: 'gpt-4-turbo-preview'
    })).choices[0].message.content;
    if (!content) {
        return;
    }

    //返信
    message.reply(content);
});
```
## 動作確認
先ほどと同様にコマンドを実行し、メッセージを送信すると文脈に沿った返答を行うことを確認します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843084/de2f8543-b1e3-10c5-be27-93321b6749a1.png)


# まとめ
今回はDiscord上でOpenAIのAPIを利用したアシスタントを作成してみました

今回は利用しませんでしたが、```gpt-4-vision-preview```というモデルを呼び出せば画像を渡すことができますし、[以前の記事](https://qiita.com/nxted_sapporo/items/43fd5f5dff2ad019cfe8)で利用した```functionCalling```という機能ではGPT経由で任意の関数を呼び出すことができて拡張性は抜群ですので、ぜひ自分専用のアシスタントを作成してみてください

## 公式ドキュメント
- Discord.js: https://discord.js.org/docs/packages/discord.js/main
- OpenAI: https://platform.openai.com/docs/api-reference