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リクエストを扱える。
計算資源の再分配
前項ではAPIリクエストについて述べたが、メインスレッドを占有しない方がいいのはすべての処理に当てはまることだ。あらゆる処理が非同期で行えるわけではないが、例えば「定期的に何らかの処理を行いたい」といったケースではtasks.loopという機能がdiscord.py側で提供されているし、場合によってはaiohttp
のように非同期処理向けのライブラリが存在しているかもしれない。もちろんコードをリファクタリングして処理時間そのものを短縮するのも有効な戦略だ。
おわりに
ユーザー数のわりにあまり知られていないDiscord.pyのTipsについて解説した。今後もこうしたお役立ち情報を見つけたときは積極的に記事に起こしていきたい。