DiscordにRAGを導入する

今回は、Discord に RAG の機能を持った bot を導入します。

## 1. botの導入
[Discord Developer Portal](https://discord.com/developers/docs/intro) にアクセスし、botを作成します。
まず、"Applications" タブから "New Application" ボタンを押し、アプリケーションの名前を入力します。
次に、"Bot" タブから "Reset Token" ボタンを押し、トークンを生成します。
このトークンは後で利用しますので、控えておいてください。
さらに、同じく "Bot" タブから "Message Content Intent" をオンにしておきます。
最後に、"OAuth2" タブから "OAuth2 URL Generator" で "bot" と "applications.commands" にチェックを入れ、"Bot Permissions" で "Send Message" と "Read Message History" にチェックを入れます。
こうすることで最下部に出現する "GENERATED URL" をコピーし、別タブでアクセスします。

これで bot の導入は完了です。

## 2. 環境構築
まず、必要なパッケージをインストールします。
```requirements.txt
python-dotenv

# ChromaDB
chromadb==0.6.2

# Discord
discord.py==2.4.0

# LangChain
langchain==0.3.14
langchain-community==0.3.14
langchain-openai==0.3.0
tiktoken==0.8.0
unstructured==0.15.5

# JupyterLab
jupyterlab==4.3.4

# OpenAI
openai==1.59.7
```
また、env ファイルに OpenAI の API キーと、先ほど控えた bot の Token を書いておきます。

## 3. RAG の実装
肝心の RAG ですが、ここは自由に実装していただいて構いません。
今回は、以下を満たす関数 `get_answer()` として実装してあるとします。
- 入力: 質問内容 `query`
- 出力: 回答 `answer`
- RAG の元データとして、 実行ファイルと同じディレクトリに存在する `messages.csv` を用いる

このもとで、 `get_answer()` を呼び出す bot 部分を以下のように実装します。
```bot.py
import csv
import discord
import openai
import os
from datetime import datetime, timedelta, timezone
from discord.ext import commands
from io import StringIO

from rag import get_answer

intents = discord.Intents.default()
intents.message_content = True
bot = commands.Bot(
    command_prefix="!",
    case_insensitive=True,
    intents=intents
)

token = os.environ['DISCORD_TOKEN']
openai.api_key = os.environ['OPENAI_API_KEY']

@bot.command(
    name="message",
    aliases=["msg", "m"],
)
async def get_message(ctx: commands.Context, channel: discord.TextChannel) -> None:
    stream = StringIO()
    data = []

    async for message in channel.history(
        after=datetime.now(timezone.utc) - timedelta(hours=1),
        oldest_first=True,
    ):
        if message.author.bot == True:
            continue
        else:
          jst = message.created_at + timedelta(hours=9)
          msg = [message.author.name, jst.strftime('%Y/%m/%d %H:%M:%S'), message.content]
          data.append(msg)

    writer = csv.writer(stream)
    writer.writerow(["User", "Timestamp", "Message"])
    writer.writerows(data)
    stream.seek(0)
    await ctx.send(file=discord.File(stream, filename="messages.csv"))
    stream.close()

@bot.command(
    name="query",
    aliases=["q"],
)
async def set_query(ctx: commands.Context, channel: discord.TextChannel, query: str) -> None:
    results = await get_answer(query)

    for result in results:
        await channel.send(result)

bot.run(token)
```
## 4. 動作サンプル
今回、`messages.csv` の中身は以下のようにしました。
一般的な知識では答えられないものや、RAG の性能を試せるようなものを用意したつもりです。
最後の文章は、みなさんご存じ太宰治の『走れメロス』から引用しています。
```messages.csv
User,Timestamp,Message
****,2025/01/14 23:44:50,私の好物はオレンジジュースです。
****,2025/01/14 23:45:06,明日の天気は霰です。
****,2025/01/14 23:46:26,私の現在位置はハワイです。
****,2025/01/14 23:49:55,明日は早起きする必要があります。
****,2025/01/14 23:50:27,私は今日4食食べました。
****,2025/01/14 23:56:56,"メロスは、単純な男であった。買い物を、背負ったままで、のそのそ王城にはいって行った。たちまち彼は、巡邏じゅんらの警吏に捕縛された。調べられて、メロスの懐中からは短剣が出て来たので、騒ぎが大きくなってしまった。メロスは、王の前に引き出された。
「この短刀で何をするつもりであったか。言え！」暴君ディオニスは静かに、けれども威厳を以もって問いつめた。その王の顔は蒼白そうはくで、眉間みけんの皺しわは、刻み込まれたように深かった。
「市を暴君の手から救うのだ。」とメロスは悪びれずに答えた。
「おまえがか？」王は、憫笑びんしょうした。「仕方の無いやつじゃ。おまえには、わしの孤独がわからぬ。」
「言うな！」とメロスは、いきり立って反駁はんばくした。「人の心を疑うのは、最も恥ずべき悪徳だ。王は、民の忠誠をさえ疑って居られる。」
「疑うのが、正当の心構えなのだと、わしに教えてくれたのは、おまえたちだ。人の心は、あてにならない。人間は、もともと私慾のかたまりさ。信じては、ならぬ。」暴君は落着いて呟つぶやき、ほっと溜息ためいきをついた。「わしだって、平和を望んでいるのだが。」
「なんの為の平和だ。自分の地位を守る為か。」こんどはメロスが嘲笑した。「罪の無い人を殺して、何が平和だ。」
「だまれ、下賤げせんの者。」王は、さっと顔を挙げて報いた。「口では、どんな清らかな事でも言える。わしには、人の腹綿の奥底が見え透いてならぬ。おまえだって、いまに、磔はりつけになってから、泣いて詫わびたって聞かぬぞ。」
「ああ、王は悧巧りこうだ。自惚うぬぼれているがよい。私は、ちゃんと死ぬる覚悟で居るのに。命乞いなど決してしない。ただ、――」と言いかけて、メロスは足もとに視線を落し瞬時ためらい、「ただ、私に情をかけたいつもりなら、処刑までに三日間の日限を与えて下さい。たった一人の妹に、亭主を持たせてやりたいのです。三日のうちに、私は村で結婚式を挙げさせ、必ず、ここへ帰って来ます。」
「ばかな。」と暴君は、嗄しわがれた声で低く笑った。「とんでもない嘘うそを言うわい。逃がした小鳥が帰って来るというのか。」
「そうです。帰って来るのです。」メロスは必死で言い張った。「私は約束を守ります。私を、三日間だけ許して下さい。妹が、私の帰りを待っているのだ。そんなに私を信じられないならば、よろしい、この市にセリヌンティウスという石工がいます。私の無二の友人だ。あれを、人質としてここに置いて行こう。私が逃げてしまって、三日目の日暮まで、ここに帰って来なかったら、あの友人を絞め殺して下さい。たのむ、そうして下さい。」
　それを聞いて王は、残虐な気持で、そっと北叟笑ほくそえんだ。生意気なことを言うわい。どうせ帰って来ないにきまっている。この嘘つきに騙だまされた振りして、放してやるのも面白い。そうして身代りの男を、三日目に殺してやるのも気味がいい。人は、これだから信じられぬと、わしは悲しい顔して、その身代りの男を磔刑に処してやるのだ。世の中の、正直者とかいう奴輩やつばらにうんと見せつけてやりたいものさ。
「願いを、聞いた。その身代りを呼ぶがよい。三日目には日没までに帰って来い。おくれたら、その身代りを、きっと殺すぞ。ちょっとおくれて来るがいい。おまえの罪は、永遠にゆるしてやろうぞ。」
「なに、何をおっしゃる。」
「はは。いのちが大事だったら、おくれて来い。おまえの心は、わかっているぞ。」"
```
このデータを元に動作させた結果、次のようになりました。
![スクリーンショット 2025-01-15 001612.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3958389/50ce8ef6-bff8-e566-8819-a6bbe132a21f.png)
![スクリーンショット 2025-01-15 001334.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3958389/0ac10f59-66d1-c4b7-c9e4-3eceb1e6ca32.png)
いずれも、与えられたデータに基づいて回答できています。
もちろん RAG 部分の実装によって性能が異なってきますが、ここまで実装できれば後は何とでもなるかと思います。
例えば、pdf などのドキュメントを元にしてみたり、複数のチャンネルのログを元にしてみたり、元データの部分だけでも様々に変更することができそうです。
## 5. まとめ
今回は、Discord に RAG の機能を持った bot を導入しました。
実際には、チャットボットとしての機能など RAG 以外にも様々な機能を持たせることができます。
皆さんも自分だけの bot を作ってみてはいかがでしょうか。