discord.pyとChatGPT(OpenAI API)で今北産業するBotコマンドを実装

# はじめに
ChatGPTが世間を席巻してから2,3ヶ月ほどでしょうか。筆者も類に漏れずおもちゃにしているわけですが、それと同時にこれを使って何か作りたいという気持ちにも駆られています。
そこで、OpenAI APIを利用してDiscord Botにその高度な自然言語処理技術を載せてしまおうと画策しました。
手始めとして完成したものは、すでに記事にしてあるのでそちらを読んでいただければと思います。
discord.pyであれば基本的なBotとスラッシュコマンドができれば流用も全く問題ないと思いますので、前回の記事の閲覧が必須ではありません。

https://qiita.com/Soli0222/items/4275e8ecc141fa27ba99

このとき、後からコマンドを追加しやすい構造に作ったので、今回はその恩恵を享受してコマンドを追加していきたいと思います。
追加するコマンドは「今北産業」です。ぼーっとしてたら思いつきました。

# ゴール
この記事で完成するBotは以下のようになります。
* コマンドはスラッシュコマンドとする
* 読み取るメッセージの範囲を時間として任意に決めることができる([/imakita 5]とすれば、今から5時間前までのメッセージを読み取る)
* メッセージを読み取り、できるだけ簡易な処理でOpenAI APIに投げてあとはGPTでどうにかしてもらう
* メッセージを3行で要約して回答する

こんなものができあがります。
![図1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287582/4183651d-cd41-28a7-7b7b-03d7b294e5b6.png)
(デバッグとしてChatGPTに作らせた会話を読み込ませています)

# 実装
前回完成したものに追加していく形になるので、最初にファイル構成を示します。コメントが書いていない部分に関しては、前回のままです。
```
/
├ cogs
│　├ imakita.py #今回つくるところ
│　└ ask.py
│
├　modules
│├ __init__.py #今回つくるところ
│└ modImakita.py #今回つくるところ
│
├ .env　
├　 main.py #1行だけ追加する
└ settings.py
```
## main.py
本当に1行追加するだけです。
```python:main.py
...

INITAL_EXTENSIONS = [
    "cogs.ask",
    "cogs.imakita" #追加した部分
]
...
```
これだけで``cogs``フォルダに``imakita.py``を追加するだけで読み込まれるので、拡張性を良くするという面を達成できていると思います。(この作業も全く自動化できる気もします)

## imakita.py
コマンドの本体を書いていきます。
```python:imakita.py
import discord
from discord.ext import commands
from discord import app_commands
import settings
import datetime
from modules.modImakita import makeReply

class ImakitaCog(commands.Cog):
    def __init__(self, bot: commands.Bot):
        super().__init__()
        self.bot = bot

    @commands.Cog.listener()
    async def on_ready(self):
        await self.bot.tree.sync(guild=discord.Object(int(settings.getId())))
        print("[Cogs] ImakitaCog is ready.")

    @app_commands.command(name="imakita", description="指定した時間分の会話を要約します")
    @app_commands.guilds(int(settings.getId()))

    #実質コーディングはここから
    async def imakita(self, interaction: discord.Interaction, hour: int):
        await interaction.response.defer()

        end_time = datetime.datetime.now()
        start_time = end_time - datetime.timedelta(hours=hour)

        text = []
        async for message in interaction.channel.history(limit=None, after=start_time, before=end_time):
            text.append(f"{message.author}: {message.content}")

        await interaction.followup.send(embed=makeReply(text,hour))
        

async def setup(bot: commands.Bot):
    await bot.add_cog(ImakitaCog(bot))
```
とはいえ、前回の``ask.py``とほぼ同じような中身になっていまして、クラス名やコンソールにログを出力する部分、登録コマンド名とその説明の部分を変えてから、``imakita``関数をちょっと書いていくくらいです。

まず回答を生成しています。これは、Botの回答が一定時間内に無いとエラーになってしまうことを回避するために、``response.defer()``で一旦回答をしてしまっています。2度目の回答については、``followup.send()``を用いて行うことができます。

次に、現在の時刻を``end_time``変数に代入しています。``datetime.timedelta(hours)``を用いて、コマンド入力時において指定されたn時間前の時刻を計算し、``start_time``変数に代入します。

最後に、``channnel.history()``を用いてメッセージを取得し、``text``リストに格納していきます。

この先の処理は別ファイルのモジュールに記述しているので、``imakita.py``はそのモジュールから渡されたものを返信として回答します。

## modImakita.py
あまり別ファイルに分けた意味が内容に思えますが、結果としてコードはスッキリしているので良しとします。コードは以下の通りです。
```python:modImakita.py
import openai
import discord
import settings

def gpt(key,texts):
    openai.api_key = key

    messages=[
            {"role": "system", "content": "会話文を3行に要約する人です"},
            {"role": "system", "content": "与えられる会話文は「[名前]#[ID]:[メッセージ]」の形式です"},
    ]

    for text in texts:
        messages.append({"role": "user", "content": text})

    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=messages
    )

    return response["choices"][0]["message"]["content"]

def makeReply(texts,hour):
    try:
        message = gpt(settings.getKey(),texts)
    except Exception as e:
        message = "回答が見つからなかったか、内部でエラーが発生した可能性があります。"
        print(e)
    
    embed=discord.Embed(title=str(hour)+"時間分の会話を要約しました", description=message, color=0x00ffff)
    return embed
```
``makeReplay``関数が呼び出されると、そのまま``gpt``関数にメッセージを格納したリストを渡します。

``gpt``関数では、OpenAI APIを扱っています。ここでまず、ChatGPTにどんな役回りをさせるかを定義します。これらのデータは``messages``変数にリスト形式で格納しておきます。
メッセージを取得した際に、``[名前]#[ID]:[メッセージ]``の形式で取得されるので、特徴量エンジニアリングみたいなことをするのがめんどくさかったので、GPTに対してこの形式で渡すと宣言しておくことで、その通りに認識してくれます。便利ですね。
また、この``messages``リストに会話のデータも追加していきます。

これであとの回答はGPTが作ってくれるので、それを``makeReply``関数に返して、埋め込みを作成したらそれが回答されます。

# 終わりに
実際に本番環境にデプロイしたは良いものの、まだサーバー内で会話がされていないので、実際の検証ができていませんが、おそらく良い感じに動いてくれると思います。

これからもなんか思いついたら追加していきたい気概です。ありがとうございました。