Toilを無くして徒然なるままに日暮し硯に向かひたい

生成AIアプリケーション開発などを行うエンジニアのブログです。

RAGアプリ開発ハンズオン(後編:フロントエンド編)

genai-users.connpass.com

上記ハンズオン勉強会の資料になります。

前回資料

shu-kob.hateblo.jp

前回の課題

  • retriever_service を定義しましたが、検索結果をcontextとして、LLMへの問い合わせを行なってください。

  • llm_serviceretriever_serviceを使うようにします。

@app.post('/api/llm')
def llm_service(question: Question):
    human_question = question.query
    model = VertexAI(model_name="gemini-2.0-flash-001", location="us-west1")
    template = """質問: {question}

    ステップバイステップで考えてください。"""

    prompt_template = PromptTemplate.from_template(template)

    chain = prompt_template | model # prompt_templateをmodelに引き渡す処理を"|"を用いて簡単に実現

    response = chain.invoke({"question": human_question}) # invokeは全ての処理が終わってから値を返す。他にはstreamなど
    print(response)
    resp = { 'answer': response }
    return resp

@app.post('/api/llm')
def llm_service(question: Question):
    human_question = question.query
    model = VertexAI(model_name="gemini-2.0-flash-001", location="us-west1")
    context_resp = retriever_service(question)
    context = context_resp['search_result']
    print(context)
    template = """質問: {question}

    以下の情報を参考にして、質問に答えてください。
    {context}
    """

    prompt_template = PromptTemplate.from_template(template)

    chain = prompt_template | model # prompt_templateをmodelに引き渡す処理を"|"を用いて簡単に実現

    response = chain.invoke({"question": human_question, "context": context}) # invokeは全ての処理が終わってから値を返す。他にはstreamなど
    print(response)
    resp = { 'answer': response }
    return resp

以下も行っておくと便利です。

  • .envを作成
DISCOVERY_ENGINE_ID=XXXXXXXXXXXXX
  • 以下の行を main.pyに追記
from dotenv import load_dotenv

load_dotenv()
  • engine_idの行を変更
@app.post('/api/retriever')
def retriever_service(question: Question):
    search_query = question.query
    project_id
    location: str = "global"
    engine_id: str = 'DISCOVERY_ENGINE_ID'

@app.post('/api/retriever')
def retriever_service(question: Question):
    search_query = question.query
    project_id
    location: str = "global"
    engine_id: str = os.environ['DISCOVERY_ENGINE_ID']
  • 動作確認
QUESTION='{"query":"情報セキュリティにおいて気をつけるべきことを教えてください"}'
curl -X POST -H "Content-Type: application/json" -d "$QUESTION" -s http://localhost:8000/api/llm | jq .

参考)ソースコード差分

retriever_serviceで得た検索結果をcontextに by shu-kob · Pull Request #4 · shu-kob/rag-app-handson · GitHub

フロントエンドの実装

フォルダ整理

これまでバックエンドを追加してきたのと同じリポジトリでフロントエンドも管理いたします。

そのためにこれまで追加してきたファイルをバックエンド用のフォルダに移動させます。

mkdir backend

# 下記以外にも必要なファイル、フォルダはbackendに移動してください。
# - __pycache__とfastapi-envは削除してください。
# - .gitがある場合は移動も削除もしないでください。
mv *.md *.py *.txt .env backend

アプリ作成

アプリの雛形を作成し、起動を確認します。

npx --yes create-react-router@latest --install --no-git-init frontend
cd frontend
npm run dev

ブラウザでhttp://localhost:5173/を開いてReact Routerの画面が表示されればOKです。

画面を変更してみる

見た目を定義しているコンポーネントはfrontend/app/welcome/welcome.tsxです。

Welcomeコンポーネントを以下のように変更します。

export function Welcome() {
  return (
    <main className="flex items-center justify-center pt-16 pb-4">
      <div className="flex-1 flex flex-col items-center gap-16 min-h-0">
        <div>
          <div>
            <label htmlFor="message">メッセージ</label>
          </div>
          <div>
            <textarea
              id="message"
              rows={4}
              cols={50}
              style={{
                padding: "0.5rem",
                border: "1px solid #ccc",
                outline: "none",
                boxShadow: "none",
              }}
            />
          </div>
          <div>
            <button
              type="button"
              style={{
                border: "1px solid #ccc",
                padding: "0.5rem 1rem",
              }}
            >
              送信
            </button>
          </div>
        </div>
      </div>
    </main>
  );
}

画面に入力欄とボタンが表示されればOKです。

入力をコントロールする

上記で入力欄に文字を入力することはできますが、その値はブラウザ側で管理されており、Reactアプリ側では取得できません。

そこでstateを用いてアプリ側で入力を制御します。

import { useState } from "react";

export function Welcome() {
  const [input, setInput] = useState("");

  const onSend = () => {
    console.log(input)
  }

  return (
    <main className="flex items-center justify-center pt-16 pb-4">
      <div className="flex-1 flex flex-col items-center gap-16 min-h-0">
        <div>
          <div>
            <label htmlFor="message">メッセージ</label>
          </div>
          <div>
            <textarea
              id="message"
              rows={4}
              cols={50}
              style={{
                padding: "0.5rem",
                border: "1px solid #ccc",
                outline: "none",
                boxShadow: "none",
              }}
              value={input}
              onChange={(e) => setInput(e.target.value)}
            />
          </div>
          <div>
            <button
              type="button"
              style={{
                border: "1px solid #ccc",
                padding: "0.5rem 1rem",
              }}
              onClick={onSend}
            >
              送信
            </button>
          </div>
        </div>
      </div>
    </main>
  );
}

テキストを入力して送信ボタンをクリックするとログにテキストの内容が表示されるようになります。

ログの確認はブラウザの開発者ツールで行います。

バックエンドとの接続

フロントエンドはバックエンドと異なるオリジンで動かしているため、CORSエラーにならないようバックエンドを修正します。

backend/main.pyに以下を追加してください。

# CORSミドルウェアの設定
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # すべてのオリジンを許可
    allow_credentials=True,
    allow_methods=["*"],  # すべてのメソッドを許可
    allow_headers=["*"],  # すべてのヘッダーを許可
    expose_headers=["*"]  # すべてのヘッダーを公開
)

変更後、バックエンドを起動します。

python -m venv fastapi-env
source fastapi-env/bin/activate

Windowsコマンドプロンプトの場合

fastapi-env/Scripts/activate
uvicorn main:app --reload

送信ボタンが押された際に入力されたテキストをバックエンドに送信し、生成AIの回答を取得できるようにします。

レスポンスの確認はブラウザの開発者ツールで行います。

  const onSend = () => {
    fetch("http://localhost:8000/api/llm", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ query: input }),
    })
  }

演習

バックエンドのResponseを画面に表示させましょう

バックエンドからのresponseをフロントエンドに表示 by shu-kob · Pull Request #6 · shu-kob/rag-app-handson · GitHub