プログラマーのメモ書き

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

RAG をチャットボットに仕立てる (1/2): FastAPI コンテナ作成

こちらの記事で Web サイトによくある『よくある質問』の内容を参照して回答する簡単な RAG を作ってみました。次は、これをチャットボット化してみようと思います。

今回目指すのは、 Wordpress で使うようなチャットボットをイメージしています。

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

API にする

いままでは、コマンドラインでループ内で質問を受け付けていたので、まずはこれを質問を投げたら答えが返ってくるような API に仕立てようと思います。

ChatGPT に聞いたところ FastAPI がおすすめとのこと。ざっと、 FastAPI について調べてみると、めっちゃ簡単に REST API を作ることができるライブラリっぽいです。

FastAPIを用いたAPI開発テンプレート #Python - Qiita

ということで、早速やってみます。

準備は、

pip install fastapi uvicorn

でいいみたいです。

FastAPI を使って、APIを作るのは、メソッド名のデコレータを書いていけばよいようです。その際、デコレータの引数に API パスを書いておけば、そのパスでアクセスしたときに応答してくれるとのことです。

from contextlib import asynccontextmanager

from fastapi import FastAPI, HTTPException, Query
from pydantic import BaseModel

import vector_index

query_engine = None


# POST のペイロードを定義
class FaqQuery(BaseModel):
    question: str = Query(
        min_length=1,
        max_length=1000,
        description="The question to query the FAQ system.",
    )

app = FastAPI(lifespan=lifespan)

# 問い合わせ エンドポイント
@app.post("/query/")
def query(payload: FaqQuery):
    """
    Handle the FAQ query.
    """
    global query_engine
    if not query_engine:
        return {"error": "Query engine is not initialized."}

    try:
        response = query_engine.query(payload.question)

        # レスポンスの整形
        result = {
            "answer": str(response),
            "references": [
                {"title": d.get("title", ""), "url": d.get("url", "")}
                for d in response.source_nodes[0].metadata.get("references", [])
            ],
        }
        return result
    except Exception as e:
        print(f"error: {str(e)}")
        raise HTTPException(status_code=500, detail=str(e))

POST で受け取るデータは、 BaseModel というのを継承してクラスを定義すればいいそうです。このとき、API呼び出しを処理する関数(この例だと query )に型ヒントがあると、送られてきたデータのバリデーションもやってっくれるそうです。めっちゃ便利。

毎回の API 呼び出しのたびに、 RAG のデータを読み込む(こちらの記事で作った、 vector_index.load_query_engine を呼びだす)のはいかにも効率が悪いので、実行時に一度だけ読ませるようにします。

ChatGPT によると @app.on_event("startup") というデコレータで定義すればいい、と出てきたんですが、すでに非推奨になっているらしくて、いまは、

# 起動前と後に実行されるのはこうやって定義するらしい
@asynccontextmanager
async def lifespan(app: FastAPI):
    """
    Application lifespan event to initialize the query engine.
    """
    global query_engine
    try:
        # Initialize the query engine
        query_engine = vector_index.load_query_engine()
        yield
    finally:
        # Cleanup if necessary
        query_engine = None

のように定義するっぽいです。

これらを1つのファイル(rag_fastapi.py)にまとめておきます。

テスト

これを呼び出して動かしてみます。コマンドラインから

.venvmor@DESKTOP-DE7IL4F:~/tmp/openai/samples/ragSample/app$ uvicorn rag_fastapi:app --reload --port 8000
INFO:     Will watch for changes in these directories: ['/home/mor/tmp/openai/samples/ragSample/app']
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [52743] using StatReload
INFO:     Started server process [52745]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

こんな感じに起動します。引数の rag_fastapi:app の部分は、実行したいファイル(rag_fastapi.py)のappオブジェクト、を指しています。 --reload はファイルに変更があった際に自動的に再読み込みしてくれるオプションだそうです。--port はいわずもがなポート番号です。

実際に API をたたいてみます。 VSCode の REST Client

という拡張機能を入れると POST でも GET でも(別のメソッドでも)簡単にリクエストを投げられるとのことなので、これで試してみます。

拡張子を .http にしたファイルを作成し、

という感じにリクエストを書きます。先頭行に、メソッド名(GETは省略可)とURL、続く行にヘッダ、1行開けて、POSTのペイロードという感じですね。

このファイルの拡張子が .http だと、URLの上側に Send Request と表示されるので、これをクリックすると

こんな感じに応答が返ってきます。いい感じやね。

(参考)別の実行方法

さきほどは uvicorn というサーバーをコマンドラインから実行しましたが、他の方法もあります。先ほどのファイルの最後に、

if __name__ == "__main__":
    import uvicorn

    # Start the FastAPI application
    uvicorn.run(app, host="127.0.0.1", port=8000)

とつけておけば、

.venvmor@DESKTOP-DE7IL4F:~/tmp/openai/samples/ragSample/container/app$ python rag_fastapi.py 
INFO:     Started server process [62108]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

とするだけでも呼び出すことができます。ご参考までに。

docker コンテナとして起動

さて、次は、 Wordpress からこの API を呼べるようにしてやる必要があります。テストなんでお手軽に試すため QNAP の Container Station を利用してコンテナを立てます。

Wordpress のコンテナのサンプルはいっぱいあるので後回しにして、まず先にさきほど作った FastAPI をdocker コンテナに仕立ててやります。

ちょっと調べてみると、こちらの記事がほぼやりたいことを実現してくれてます。

DockerでFastAPIの環境を作ってGETするまで

ということで、これに従って作業します。

なお、今回作業したフォルダはこんな感じにしました。

.
├── .env
├── Dockerfile
├── app
│   ├── __index__.py
│   ├── faiss_index
│   │   ├── default__vector_store.json
│   │   ├── (略)
│   │   └── index_store.json
│   ├── faq_dict.py
│   ├── rag_fastapi.py
│   └── vector_index.py
├── docker-compose.yml
└── requirements.txt

docker-compose.yml

コンテナを起動するための docker-compose.yml は次のようにします。

services:

  fastapi:
    container_name: fastapi
    build: .
    restart: always
    ports:
      - 8000:8000
    environment:
      - OPENAI_API_KEY

ここで、 OpenAI の API キーは .env に書いておいてこれを渡すようにします。

# added by Junichi MORI, 2025/6/5, for openai api
OPENAI_API_KEY="API キー"

Dockerfile

docker-compose.yml では Dockerfile からイメージを作っています。この Dockerfile は次のようにしました。

FROM python:3.13-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY ./app .

CMD ["uvicorn", "rag_fastapi:app", "--reload", "--host", "0.0.0.0", "--port", "8000"]

requirements.txt

LlamaIndex の処理と FastAPI を実行するために必要なものを書いておきます。

llama-index
faiss-cpu
llama-index-vector-stores-faiss
openai
fastapi
uvicorn

ビルドとコンテナの起動

最初は、 Contaienr Station の『アプリケーション』から docker-compose.yml を与えて、コンテナを起動させます。もちろんこの段階では、起動に失敗します。

次に、上記の一式をローカルで作成して、これらを QNAP NAS の Container Station 所定のアプリケーションフォルダに ssh でログインしてコピーします。

ssh でアプリケーションフォルダにいる状態で、

[user1@nas01 rag-test]$ docker compose build
[+] Building 7.5s (11/11) FINISHED                                                                                                                                                                                       docker:default
(略)

のようにして、イメージをビルドしておきます。

次に、 Contaienr Station の画面に戻って、 アプリケーション全体を再起動します。問題がなければ、 FastAPI がコンテナで起動されます。

Wordpress コンテナを追加

ここまでできたら、この FastAPI のコンテナを呼び出すため、 docker-compose.yml に Wordpress のコンテナを追加します。

services:

  fastapi:
    container_name: fastapi
    build: .
    restart: always
    ports:
      - 8000:8000
    environment:
      - OPENAI_API_KEY

  wordpress:
    image: wordpress
    restart: always
    ports:
      - 8080:80
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: exampleuser
      WORDPRESS_DB_PASSWORD: examplepass
      WORDPRESS_DB_NAME: exampledb
    volumes:
      - wordpress:/var/www/html

  db:
    image: mysql:8.0
    restart: always
    environment:
      MYSQL_DATABASE: exampledb
      MYSQL_USER: exampleuser
      MYSQL_PASSWORD: examplepass
      MYSQL_ROOT_PASSWORD: wordpress
    volumes:
      - db:/var/lib/mysql

volumes:
  wordpress:
  db:

Wordpress に関する部分は、 DockerHub の Wordpress のサンプルから持ってきました。

QNAP NAS の ContainerStation の画面から、さきほど動かした FastAPI のアプリケーションを選択して、歯車アイコンをクリックして、『再作成』を選びます。

すると、docker-compose.yml を記入する画面が表示されるので、上記の Wordpresss を追加した内容に書き換えます。

このまま『更新』をクリックすると、3つのコンテナからなるアプリケーションが起動します。

ちょっと面倒なのが、この方法で再作成すると、 fastapi コンテナを作ったときのファイル等はいったん削除されてしまいます(フォルダごと削除されて、再作成されます)。fastapi イメージが残っているので、 fastapi コンテナの起動はできているのですが、 .env ファイルも消されているので、 docker-compose.yml に書いている OpenAI API キー が引き渡されないため正しく動作しません。

実際、 fastapi コンテナにリクエストしても、こんな感じに

Connection Error となって返ってきます。

なので、 .env ファイルを再度コピーしておきます。faiss インデックスのファイルや Dockerfile はすぐには不要なのですが、イメージの再ビルドに備えて、一緒に他のファイルもコピーしておきます。

ということで、 .env ファイルコピー後に、もう一度 fastapi のコンテナを作り直しておきます。

[user1@nas01 rag-test]$ docker compose up -d --no-deps --force-recreate fastapi

Wordpress のセットアップ

コンテナが問題なく起動したら、指定したポート番号(この例だと 8001)へアクセスしてみます。すると、 こんな感じの

言語選択画面が表示されます。『日本語』を選択して、進むと、引き続いて、

こんな感じの初期設定画面(管理者ユーザー名とパスワードの指定画面)が表示されるので、必要な項目を入力して、一番下の『WordPress をインストール』ボタンをクリックしてしばらく待つと、

と画面が表示され、セットアップ完了です。

この状態で、問題なくサイトを表示したり、管理画面にログインできることを確認しておきます。

まとめ

さて、これで準備ができました。次はチャットボットを実装していきます。