🤖

discord.pyとgpt-3.5-turboでDiscord BotにスラッシュコマンドでChatGPTのコマンドを導入する

2023/03/13に公開

概要

  • discord.py
  • スラッシュコマンド
  • 最新のgpt-3.5-turbo

でDiscord BotにChatGPTの機能を導入する際の手順と気をつけるポイントを紹介します。

今回はOpenAIが用意しているライブラリを使用する方法ではなく、HTTPXを使ってChat Completionのエンドポイントを非同期で叩きます。

HTTPXを使用する理由


HTTPX
discord.pyの2.x系でスラッシュコマンドを実装したBOTからChatGPTのエンドポイントを叩いたときに問題になるのが「レスポンスの遅さ」です。

このレスポンスの遅さがブロッキングを起こしてほぼエラーになります。

これは前回書いた、interactionsへのレスポンスは3秒以内に返す必要がある
https://zenn.dev/quojama/articles/63fdc637a3020b#interactionsへのレスポンスは3秒以内に返す必要がある

こちらとはまた別の話です。
現状私の中では、Discord BotでOpenAIのAPIを叩くときは非同期でのリクエストが必須ではないかなと思っています。

なので今回はHTTPXを使って、非同期リクエストをします。

OpenAIのライブラリは非同期でリクエストできないの?

デフォルトでは非同期ではありません。Requests: HTTP for Humans™と同じく、内部では同期型のリクエストを行っているようです。

一応以前 (gpt-3.5-turboが出る前のDavinci時代)はライブラリでも非同期でリクエストするオプションがありました。

参考:
openai/openai-python: The OpenAI Python library provides convenient access to the OpenAI API from applications written in the Python language.

しかし今回のChat Completionでは今のところasyncでのリクエストの記述を見つけられなかったので、現状は無いかもしれません。(方法あったら教えてください)

サンプル

.
├── src/
│   └── cogs/
│       └── openai.py
└── main.py

src/cogs/openai.py

コメントアウトで説明をなるべく丁寧に書きます。

import os

import discord
import httpx
from discord import app_commands
from discord.ext import commands

# かかった料金を計算するため別途別のAPIでドル円のレートを取ってきています。
# 無視してOKです。
from libs.utils import get_exchange_rate

class Openai(commands.Cog):
    def __init__(self, bot: commands.Bot):
        self.bot = bot
	# デフォルトのキャラクターを設定します。
        self.default_character = "あなたは、下町の大将です。優しさはありますが、口調は乱暴です。敬語は使わなくてよいです。"

    # ここではDiscordのスラッシュコマンドの設定です。
    # /gpt {key} {character} です。
    # keyは質問内容、characterはキャラ設定を決められます。
    # characterは省略可能で、省略した場合はデフォルトの寿司屋の大将になります。
    @app_commands.command(
        name="gpt",
        description="ChatGPTに質問をしましょう!"
        )

    @app_commands.describe(
        key="質問内容",
        character="ChatGPTに性格やキャラを与えることができます。必ず「あなたは~です。」と書いてください。"
        )

    async def openai(
        self,
        interaction: discord.Interaction,
        key: str,
	# デフォルト値を決めているので省略可能。
        character:str = None
        ):

        if character is None:
            character = self.default_character

        # ほぼ確実に3秒以内ではレスポンスできないのでこの処理を入れる。
	# 前回の記事を参考にしてください。
        await interaction.response.defer()

        # ここからAPIリクエストの処理です。
	# エンドポイントのURLはこちら
        endpoint = "https://api.openai.com/v1/chat/completions"
        
	# headerの内容はこちらです。Bearerで認証を通します。
        headers = {
            'Content-Type': 'application/json',
            'Authorization': f'Bearer {os.getenv("OPENAI_API_KEY")}'
        }

        # payloadはbodyのrawです。とりあえずmax_tokensだけ入れています。
        payload = {
            "model": "gpt-3.5-turbo",
            "messages" : [
                {"role": "system", "content": character},
                {"role": "user", "content": key}
            ],
            "max_tokens": 1000
        }

	# 一応HTTPリクエストでエラーがあったときの簡単な例外処理も入れておきます。(大事)
	# timeout=120は必ず入れてください。
	# 120である必要はないですが、デフォルトの値が5秒なので、5秒でレスポンスが帰ってこないとエラーになります。
	# 5秒は余裕で超えますのでとりあえず私は2分としています。
	# Noneにすると、一生諦めずにリクエストし続けるのでこれはこれで問題ですのでおすすめしません。
        try:
            async with httpx.AsyncClient() as client:
                res = await client.post(endpoint, headers=headers, json=payload, timeout=120)
        except httpx.HTTPError as e:
            await interaction.followup.send(f"⚠ APIリクエストエラーが発生しました。時間を置いて試してみてください。: {e}")
            return

        if res.status_code != 200:
            await interaction.followup.send(f"⚠ APIリクエストエラーが発生しました。時間を置いて試してみてください。\n Status Code: {res.status_code}")
            return
	
	# jsonゲットです。
        json = res.json()

	# 返答内容と、使用したトークンを取得します。
        answer = json["choices"][0]["message"]["content"]
        tokens = json["usage"]["total_tokens"]
	
	# 今回の質問に日本円でいくらかかったか計算しています。
	# いらなかったら省いてください。
        cost = round(tokens * 0.000002 * get_exchange_rate(), 3)
	
	# 今回のキャラ設定を表示させるための処理です。
        if character == self.default_character
            character = "Default"
	
	# embedを作ります。
        embed = discord.Embed()
        embed.title = f"Q. {key}"
        embed.description = answer
        embed.color = discord.Color.dark_green()
        embed.set_footer(text=f"🤖 キャラ設定: {character}\n💸 この質問の料金は {cost}円 でした。")
	
	# 最後に投稿します。
        await interaction.followup.send(embed=embed)

async def setup(bot: commands.Bot):
    await bot.add_cog(Openai(bot), guilds=[discord.Object(id=xxxxxxxxxxxxxxxxx)])

こんな感じです。対話は実装していません。複数人が同時に使うチャットでどのように設計するのがいいか考えるのがめんどくさかったのでとりあえず一回のみの質問です。


(全然寿司屋の大将っぽくない)

まとめ

  • 非同期でリクエストせよ!
  • Discord Botは最初からExtensionsを使って機能でファイルを切り分けることを強くおすすめします!

私が作ってるUseless Botのリポジトリはこちら
pistachiostudio/takohachi: gangsta discord bot ᕦ(+_+;)ᕤ

Discussion