プログラマーのメモ書き

伊勢在住のプログラマーが気になることを気ままにメモったブログです

RAG をチャットボットに仕立てる (2/2):チャットボットの実装

次は、チャットボット部分の実装です。チャットボットもいろいろとありますが、今回は、 Wordpress のプラグインとして動くものにしようと思います。

なお、以下の関連記事がありますので、こちらもよろしく。

チャットボット作成

チャットボットの作り方も ChatGPT に聞いてみました。教えてくれた方法は、ショートコードで実装する方法です。

必要なファイルの作成

まずは、 chatbot-plugin.php というチャット用の機能および UI を Wordpress に追加するコードです。

<?php
/*
Plugin Name: RAG Chatbot Plugin
Description: チャットUIをWordPressに埋め込む
Version: 1.0
Author: あなたの名前
*/

function chatbot_enqueue_scripts() {
    wp_enqueue_style('chatbot-style', plugin_dir_url(__FILE__) . 'chat-style.css');
    wp_enqueue_script('chatbot-js', plugin_dir_url(__FILE__) . 'chatbot.js', array(), null, true);
}
add_action('wp_enqueue_scripts', 'chatbot_enqueue_scripts');

function chatbot_display() {
    ob_start();
    ?>
    <div id="chatbot-container">
        <div id="chat-window"></div>
        <input type="text" id="chat-input" placeholder="質問を入力..." />
        <button id="chat-send">送信</button>
    </div>
    <?php
    return ob_get_clean();
}
add_shortcode('chatbot', 'chatbot_display');

Wordpress が用意している API をいくつか呼び出して、必要な機能の登録を行っています。また、先頭部分のコメントは、 Wordpress でプラグインとして登録する際に必要になるとのことです。

次は、この UI に対する操作を行ったときに呼ばれる、 FastAPI コンテナと通信するための JS コードになります。

document.addEventListener("DOMContentLoaded", function () {
    const input = document.getElementById("chat-input");
    const sendButton = document.getElementById("chat-send");
    const chatWindow = document.getElementById("chat-window");

    function appendMessage(message, references, sender) {
        const msgDiv = document.createElement("div");
        msgDiv.className = `chat-message ${sender}`;
        msgDiv.textContent = message;
        // 参照がある場合はリンクを追加
        if (references && references.length > 0) {
            const refDiv = document.createElement("div");
            refDiv.className = "chat-references";
            refDiv.appendChild(document.createTextNode("参考情報: "));
            refDiv.appendChild(document.createElement("br"));
            for (let i = 0; i < references.length; i++) {
                const refLink = document.createElement("a");
                refLink.href = references[i].url;
                refLink.textContent = references[i].title;
                refLink.target = "_blank";
                refDiv.appendChild(refLink);
                if (i < references.length - 1) {
                    //refDiv.appendChild(document.createTextNode(", "));
                    refDiv.appendChild(document.createElement("br"));
                }
            }
            msgDiv.appendChild(refDiv);
        }
        chatWindow.appendChild(msgDiv);
        chatWindow.scrollTop = chatWindow.scrollHeight;
    }

    sendButton.addEventListener("click", async () => {
        const text = input.value.trim();
        if (!text) return;

        appendMessage(text, null, "user");
        input.value = "";

        try {
            // JS はブラウザ上で実行されるので、ブラウザから見た FastAPI サーバーの URL になる
            const response = await fetch("http://nas01:8000/query/", {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ question: text })
            });

            const data = await response.json();
            appendMessage(data.answer, data.references, "bot");
        } catch (error) {
            appendMessage("エラーが発生しました。", "bot");
            console.error(error);
        }
    });
});

appendMessage 関数で、RAG からの結果に参考URLが含まれていた場合の処理を書いてるので、ちょっと長ったらしくなってますが、やってることは難しくありません。

あとは、この UI の CSS ファイルです。

#chatbot-container {
    width: 300px;
    border: 1px solid #ccc;
    padding: 10px;
    font-family: sans-serif;
}
#chat-window {
    height: 200px;
    overflow-y: auto;
    border: 1px solid #eee;
    margin-bottom: 10px;
    padding: 5px;
    background: #fafafa;
}
.chat-message {
    margin-bottom: 5px;
}
.user { text-align: right; color: blue; }
.bot { text-align: left; color: green; }

chat-message と共に設定するクラスの値(user, bot)に応じて、テキストの色と位置を変えています。

Wordpress コンテナにコピー

上記のファイルを作成したら、 chatbot-plugin というフォルダにまとめておきます。これをフォルダごと、Wordpress コンテナにコピーしてやります。

[user1@nas01 rag-test]$ docker cp ./chatbot-plugin/ rag-test-wordpress-1:/var/www/html/wp-content/plugins/
Successfully copied 6.14kB to rag-test-wordpress-1:/var/www/html/wp-content/plugins/
[user1@nas01 rag-test]$ 

FastAPI の CORS 設定を変更

あと、 FastAPI 側で Wordpress のサーバーからの通信を許可するために、 CORS 設定を追加してやります。

app = FastAPI(lifespan=lifespan)

app.add_middleware(
    CORSMiddleware,
    # allow_origins=["*"],  # Allow all origins for CORS
    allow_origins=[
        "http://nas01:8001"
    ],  # Wordpress の URL, コンテナ外からのアクセスの形式
    allow_credentials=True,
    allow_methods=["*"],  # Allow all methods
    allow_headers=["*"],  # Allow all headers
)

ここでのホスト名(オリジン)は docker-compose.yml のサービス名ではなく、docker 外からみた FastAPI サーバーのURLになります。

FastAPI の内容が変わってしまっているので、一度ビルドしなおします。

[user1@nas01 rag-test]$ docker compose stop fastapi
[+] Stopping 1/1
 ✔ Container fastapi  Stopped
[user1@nas01 rag-test]$ docker compose build --no-cache fastapi
[+] Building 130.2s (11/11) FINISHED
(略)
[user1@nas01 rag-test]$ docker compose up -d --no-deps --force-recreate fastapi
[+] Running 1/1
 ✔ Container fastapi  Started
[user1@nas01 rag-test]$ 

チャットボットの呼び出し部分を追加

最後に、チャットボットを呼び出せるようにします。Wordpress のサイトへアクセスして、管理画面にログインして『プラグイン』を表示します。

このように追加したショートコード(プラグイン)が表示されるので、『有効化』をクリックします。

Wordpress の固定ページ一覧を開くと、

『サンプルページ』というのがあるので、これを編集して、一番下に追加したチャットボット用のショートコードを埋め込みます。

ショートコードの名前は、チャットボットの登録処理 add_shortcode('chatbot', の第1引数になります。

編集後保存して、サンプルページを表示すると、

こんな感じに、入力欄が表示されていれば OK です。

テスト

さて、テストします。チャットボットの画面で、質問を入力して『送信』ボタンを押すと、

こんな感じに回答と参考情報があればそれへのリンクが表示されます。問題なさそうですね。

失敗例

実は、最初から成功したわけではなく、最初は

こんな感じになってしまいました。このとき、ブラウザは Forefox を使っていたのですが、開発者ツールを見ると、

こんな感じのエラーになってました。開発者ツールのネットワークタブをみても、

という感じで、 OPTIONS で failed となっているので、てっきり CORS 関係の設定かな?と思い、 FastAPI 側の CORS 設定の書き方をいろいろと試してみたのですが、なかなか解消しません。

そのうち、(何がきっかけか覚えていませんが)ブラウザの CORS を無効にする方法が載ってる記事にあたりました。

ブラウザでCORSを無効化する方法 #Security - Qiita

言われてみれば、たしか CORS って同一オリジンポリシーを保証するためにブラウザで検証されているんだよな?と思い出し、改めて、 CORS を無効にしたら問題なく通信できるはずだ、と考えて試すことにしました。

ということで、このとき使っていた Firefoxの CORS を無効にして

試します。でも、同じエラーが返ってきます。

開発者ツールのネットワークタブをみると OPTIONS のリクエストはなくなっているので、 CORS が無効になっているのは間違いなさそうです。

ん?これはなんか変だな? CORS に関係なくエラーになっているということか?と思ったので、 Edge で試すと、

『ERR_NAME_NOT_RESOLVED』とエラーがでてきます。名前が解決できない?

・・・あ、よくよく考えると、JaaScriptなんでこの処理はブラウザで実行されています。ということは、ホスト名もブラウザ上で解決できないといけません。

実は、このエラーが起きていた当初は、この部分のリクエストを

        try {
            const response = await fetch("http://fastapi:8000/query", {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ question: text })
            });

としていました。これですね。

この fastapi というのは、 docker compose でのサービス名です。 Wordpress コンテナから FastAPI コンテナへの通信と思ってしまったので、同じ docker network 上にあるということで、 docker-compose.yml のサービス名を指定する形にしてしまっていました(コンテナ間で通信できるのが気になったので、 Wordpress のコンテナに入って curl http://fastapi:8000 のようにして、応答返ってくるのも確認してました)。

でも、JS が実行されるブラウザ上では、docker compose のサービス名で指定されてもわかんないですよね、そりゃ。

ということで、この部分を FastAPI サーバーを指すように変更して試すと、はい、うまくいきました。

これに気づいてから、ネットを調べると、やっぱり同じようなところではまる方もいるんですね。

ruc-4130.hatenablog.com

最初にこの記事見つけられてれば、苦労しなかったのにな・・・

フローティングチャットウィンドウ

これでも一応はチャットボットなんですが、Web サイトによくある画面の右下に出てきて、クリックするとウィンドウが開いてチャットができるやつも作ってみたいと思います。

必要なファイルの作成

FastAPI 側の設定(CORS)とかはさきほどと同じでよいので、 Wordpress 側だけ追加のコードを用意します。

<?php
/*
Plugin Name: RAG Floating Chatbot
Description: 全画面で表示されるフローティングチャットボット
Version: 1.0
Author: あなたの名前
*/

function rag_chat_assets() {
    wp_enqueue_style('rag-chat-style', plugin_dir_url(__FILE__) . 'assets/chatbot.css');
    wp_enqueue_script('rag-chat-js', plugin_dir_url(__FILE__) . 'assets/chatbot.js', array(), null, true);
}
add_action('wp_enqueue_scripts', 'rag_chat_assets');

function rag_chat_html() {
    ?>
    <div id="rag-chat-icon">&#128172;</div>
    <div id="rag-chat-box" style="display:none;">
        <div id="rag-chat-header">チャットボット <span id="rag-chat-close">×</span></div>
        <div id="rag-chat-messages"></div>
        <div id="rag-chat-input-area">
            <input type="text" id="rag-chat-input" placeholder="質問を入力..." />
            <button id="rag-chat-send">送信</button>
        </div>
    </div>
    <?php
}
add_action('wp_footer', 'rag_chat_html');

UI の開閉や FastAPI サーバーと通信を行うための JS ファイルを用意します。

document.addEventListener("DOMContentLoaded", function () {
    const icon = document.getElementById("rag-chat-icon");
    const box = document.getElementById("rag-chat-box");
    const close = document.getElementById("rag-chat-close");
    const send = document.getElementById("rag-chat-send");
    const input = document.getElementById("rag-chat-input");
    const messages = document.getElementById("rag-chat-messages");

    icon.addEventListener("click", () => {
        box.style.display = "flex";
        icon.style.display = "none";
    });

    close.addEventListener("click", () => {
        box.style.display = "none";
        icon.style.display = "block";
    });

    function appendMessage(message, references, sender) {
        const msgDiv = document.createElement("div");
        msgDiv.className = `rag-msg ${sender}`;
        msgDiv.textContent = message;
        // 参照がある場合はリンクを追加
        if (references && references.length > 0) {
            const refDiv = document.createElement("div");
            refDiv.className = "chat-references";
            refDiv.appendChild(document.createTextNode("参考情報: "));
            refDiv.appendChild(document.createElement("br"));
            for (let i = 0; i < references.length; i++) {
                const refLink = document.createElement("a");
                refLink.href = references[i].url;
                refLink.textContent = references[i].title;
                refLink.target = "_blank";
                refDiv.appendChild(refLink);
                if (i < references.length - 1) {
                    //refDiv.appendChild(document.createTextNode(", "));
                    refDiv.appendChild(document.createElement("br"));
                }
            }
            msgDiv.appendChild(refDiv);
        }
        messages.appendChild(msgDiv);
        messages.scrollTop = messages.scrollHeight;
    }

    send.addEventListener("click", async () => {
        const text = input.value.trim();
        if (!text) return;

        appendMessage(text, null, "user");
        input.value = "";

        try {
            const res = await fetch("http://nas01:8000/query/", {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ question: text })
            });
            const data = await res.json();
            appendMessage(data.answer, data.references, "bot");
        } catch (err) {
            appendMessage("エラーが発生しました。", "bot");
            console.error(err);
        }
    });

    input.addEventListener("keydown", (e) => {
        if (e.key === "Enter") send.click();
    });
});

FastAPI の呼び出し部分は最初のやつと同じです。それ以外はアイコンをクリックして、チャットウィンドウを開いたり反対に閉じたりという処理ですね。

最後に、 CSS ファイルです。

#rag-chat-icon {
    position: fixed;
    bottom: 20px;
    right: 20px;
    background: #007bff;
    color: white;
    width: 60px;
    height: 60px;
    font-size: 30px;
    border-radius: 50%;
    text-align: center;
    line-height: 60px;
    cursor: pointer;
    z-index: 9999;
}

#rag-chat-box {
    position: fixed;
    bottom: 90px;
    right: 20px;
    width: 300px;
    height: 400px;
    background: white;
    border: 1px solid #ccc;
    box-shadow: 0 0 10px rgba(0,0,0,0.3);
    z-index: 9999;
    display: flex;
    flex-direction: column;
}

#rag-chat-header {
    background: #007bff;
    color: white;
    padding: 10px;
    font-weight: bold;
    display: flex;
    justify-content: space-between;
}

#rag-chat-close {
    cursor: pointer;
}

#rag-chat-messages {
    flex: 1;
    overflow-y: auto;
    padding: 10px;
    background: #f9f9f9;
}

#rag-chat-input-area {
    display: flex;
    padding: 10px;
    border-top: 1px solid #ccc;
}

#rag-chat-input {
    flex: 1;
    padding: 5px;
}

#rag-chat-send {
    padding: 5px 10px;
    margin-left: 5px;
}

.rag-msg.user {
    text-align: right;
    margin-bottom: 5px;
    color: #007bff;
}

.rag-msg.bot {
    text-align: left;
    margin-bottom: 5px;
    color: #28a745;
}

Wordpress コンテナにコピー

上記のファイルを作成したら、 rag-floating-chat フォルダにまとめておきます。これをフォルダごと、Wordpress コンテナにコピーしてやります。

[user1@nas01 rag-test]$ docker cp ./rag-floating-chat/ rag-test-wordpress-1:/var/www/html/wp-content/plugins/
Successfully copied 8.7kB to rag-test-wordpress-1:/var/www/html/wp-content/plugins/
[user1@nas01 rag-test]$ 

テスト

Wordpress の管理画面に入って、『プラグイン』を開き、

を有効にします。この状態で、画面を表示すると、

のように右下にチャットのアイコンが表示されます。

クリックすると、

こんな感じで広がり、質問を入力すると、

先ほどと同じように回答してくれます。

なかなかいいですね。

まとめ

RAG の実装もチャットボットの実装も ChatGPT (+ Gemini )に聞きながらやったら、結構手早くできました。RAG自体はいろいろと可能性があるな、と感じているんですが、いかんせん押さえておくべきたくさんありますね。

あと、今回試していて思ったのは、わざわざ RAG を使わなくても、公開されているサイトであれば

公開されているサイトの URL をプロンプトに含めて、 OpenAI API を呼び出す

とするだけで、結構いいものができるのかもしれないというところです。実際 Wordpress のプラグインを調べると、生成 AI を利用したものがたくさん見つかります。

クローズドな非公開情報であれば、当然 RAG は有効になってくるんでしょうが、なかなか判断も難しいものがあるように思えます。

その他にも LlamaIndex についてあれこれ調べていると、

llama-indexとRAGに関する話 - matobaの備忘録

というのがあり、 LlamaIndex がクローラをプラグインで持っているという話が載ってました。実際に見てみると、webページを簡単にクローリングする機能も追加できそうです。

Web Page Reader - LlamaIndex

これなども使うと、また RAG のカバー範囲とか精度とかも変わってきそうな感じがします。

その他にも RAG の先には fine-tuning もあるし、なかなか大変な分野っぽいです。注目されている分野ですから、折に触れて、いろいろと試してみようと思います。