OpenAIのライブラリを使わずにChatGPT APIをDiscord Botに組み込む

ChatGPT APIをPythonから扱うベストプラクティス

  • Python
  • Discord.py

投稿日:

もくじ

はじめに

友人たちとのグループ内で運用しているDiscord BotにChatGPT APIを統合した。

ChatGPT APIやその他のブロッキング時間が長い処理をDiscord.pyライブラリと組み合わせる際のベストプラクティスについても気付きがあったので、簡単なハウツーと併せて記す。

コード全文

本稿ではDiscord ApplicationとOpenAI APIのトークン取得やDiscord.pyの使い方についての説明は割愛する。

なお、以下のコードでは読みやすさのために型アノテーションを使っているが、Pythonのバージョンが古く型アノテーションに対応していないという場合は適宜型情報を引っぺがすなどしてほしい。

from typing import Literal
import aiohttp
import discord

ChatGPT_Models = Literal["gpt-3.5-turbo", "gpt-4"]

OPENAI_API_ENDPOINT = "https://api.openai.com/v1/chat/completions"
CHAT_INSTRUCTIONS = "ユーザーに好意を持つツンデレの女の子として振る舞ってください。"


async def main(prompt: str, token: str, message: discord.Message, session: aiohttp.ClientSession, model: ChatGPT_Models):
    async with message.channel.typing():
        request_headers = [
            ("Content-Type", "application/json"),
            ("Authorization", f"Bearer {token}"),
        ]
        request_body = {
            "model": model,
            "messages": [
                {
                    "role": "system",
                    "content": CHAT_INSTRUCTIONS
                },
                {
                    "role": "user",
                    "content": prompt,
                },
            ],
            "temperature": 0.3,
        }

        async with session.post(
            OPENAI_API_ENDPOINT, json=request_body, headers=request_headers
        ) as raw_response:
            response = await raw_response.json()
            response_content = response["choices"][0]["message"]["content"]

        await message.reply(response_content)


async def chat(prompt: str, token: str, message: discord.Message, model: ChatGPT_Models):
    async with aiohttp.ClientSession() as session:
        await main(prompt, token, message, session, model)

上記のコードをメインのスクリプトと同じディレクトリに置いてimport文で読み込むなりスクリプト内に直接コピペするなりしたあと、chat関数にプロンプトとOpenAI APIのトークン、discord.pyのメッセージオブジェクト、使いたいChatGPTのモデル名を渡してやればあとは勝手にメッセージに返信してくれる。

なお、chat関数を直接呼んでも非同期で処理されない。asyncio.create_taskを使って一度タスクを生成してから、そのタスクを呼び出す必要がある。文章だけだと分かりにくいので、一応サンプルコードも掲載しておく。

import asyncio

from chat import chat # 上記のコードをライブラリとして読み込む

# ... メッセージ受信時の処理
chat_task = asyncio.create_task(chat(prompt, token, message, model))
await chat_task

systemロールのメッセージを設定する定数CHAT_INSTRUCTIONSにはChatGPTに与える命令(Web版・モバイル版の”Custom Instructions”に相当)を入力する。ユーザーに好意を持つツンデレの女の子キャラがいいのであれば変更しなくてもよい。

Discord Botのバックエンドで重い処理をするときのベストプラクティス

上記のコードではOpenAIから提供されているopenaiライブラリを使うかわりにaiohttpなる見慣れないライブラリでREST APIを直接叩いているが、これにはどのような意味があるのだろうか?

実は、Discord.pyと重い処理を組み合わせる時には注意点がある。本項ではその注意点と対処方法について解説する。

機械にも血は通っている

……と書くと大げさだが、要するにDiscord.py(とその他のDiscord APIのラッパーライブラリ)は定期的にDiscord側のサーバーとHeartbeat(心拍)と呼ばれるシグナルを送受信して接続を維持しているのだ。

ゆえに、バックエンドでメインスレッドを長時間ブロックする処理を行うとHeartbeatを送れず接続が切れて意図しない挙動をしたり、そもそも処理中に送られてきたメッセージが読めなくなったりする(=複数人と同時に会話できなくなる)などの問題が発生する。

aiohttpで複数のHTTPリクエストを同時に処理する

前述のとおり、Discord Botのバックエンドで時間のかかる処理を行うとバグやUX低下の元となる。しかしChatGPTのAPIを叩いた時に重い処理が行われるのはOpenAIのサーバー側であって、Botのバックエンド側ではないはずだ。つまりAPIからレスポンスが返ってくるまでの間はメインスレッドを解放してやることで、複数のメッセージに同時に返事をできるはずである。そこで登場するのがaiohttpだ。aiohttpを使えば非同期にHTTPリクエストを扱える。

requestライブラリとaiohttpライブラリを利用したAPI呼び出しの比較
requestライブラリとaiohttpライブラリを利用したAPI呼び出しの比較

計算資源の再分配

前項ではAPIリクエストについて述べたが、メインスレッドを占有しない方がいいのはすべての処理に当てはまることだ。あらゆる処理が非同期で行えるわけではないが、例えば「定期的に何らかの処理を行いたい」といったケースではtasks.loopという機能がdiscord.py側で提供されているし、場合によってはaiohttpのように非同期処理向けのライブラリが存在しているかもしれない。もちろんコードをリファクタリングして処理時間そのものを短縮するのも有効な戦略だ。

おわりに

ユーザー数のわりにあまり知られていないDiscord.pyのTipsについて解説した。今後もこうしたお役立ち情報を見つけたときは積極的に記事に起こしていきたい。

著者プロフィール

著者近影

橘いおね

運用より開発を愛する日曜Web大工Web weekend developer。2021年からWeb開発の勉強を始め、スクラップ&ビルド&スクラップを繰り返して現在に至る。趣味はビデオゲーム(あまり上手ではない)。