Chatbot UI (オープンソースの ChatGPT UI クローン) を Vercel でホストする

ChatGPT を、その UI に近い状態のままローカル環境で実行可能にする、Chatbot UI を、Vercel でホストする手順を簡単にまとめつつ、Next.js の Middleware でベーシック認証を追加する簡単なカスタマイズについて紹介。

ChatGPT を、その UI に近い状態のままローカル環境で実行可能にする、Chatbot UI というオープンソースの Web アプリケーションがあります。要するに API 利用者向けに、本家同様の Web UI を提供してくれる便利なツールです。

本家の ChatGPT にない利点として、API 経由でのアクセスとなるため、ChatGPT の 標準 UI を使用した場合と異なり、データを入力したデータが OpenAI によって学習データとして利用されることがない (オプトアウト自体は可能ですが申請が必要 / 参考リンク) 点や、チャット履歴をインポート / エクスポートする機能、あるいはよく使うプロンプトをテンプレート化して保存し、ショートカットから簡単に呼びさせる機能など、今のところ、本家の ChatGPT 標準 UI にはない便利機能がある点などが挙げられます。

で、ローカルで動かすのはめちゃくちゃ簡単で、OpenAI の API Key を持ってて、あとはローカル環境に Node.js 環境と Docker さえ入っていれば数分で立ち上げることができます。

OpenAI の API Key については、過去に下記の記事で触れたので参考まで。

ローカルで動作させるだけでも十分なんですが、Chatbot UI は Next.js を使用した Web アプリケーションですから、Node.js 環境があれば外部サーバでホストすることもできます。今回は、Vercel でホストする手順を簡単にまとめつつ、Next.js の Middleware でベーシック認証を追加したりといった簡単なカスタマイズについて触れておきたいと思います。

下準備

まず、下記を準備します。持っていない人はアカウントを作るとか、必要に応じて課金するとか、インストールするといったことをやってください。

  • OpenAI の API Key を取得する (ChatGPT とは別に課金が必要なので注意)
  • Docker Desktop をインストールする
  • Node.js 環境を整える
  • Supabase (データベース) でアカウントを作っておく (プロジェクトの新規作成や設定については後述します)
  • Vercel アカウント

Chatbot UI を Vercel でホストする

基本的には Chatbot UI の README.md に書かれている手順通りにすればよいのですが、1点だけ、必要な手順が抜けているので、それを踏まえて順を追って説明します。

ちなみに私は普段、Windows 環境なのでその前提で書いていますが、Mac や Linux 環境の方はそれぞれの環境に合わせてください。

ローカルにリポジトリをクローンして環境構築

まずは Chatbot UI のリポジトリをローカルに持ってきます。

git clone https://github.com/mckaywrigley/chatbot-ui.git

必要なパッケージをインストールします。

npm install

Scoop をインストール

すでに入っているという方は飛ばして大丈夫ですが、Scoop をインストールします。

iwr -useb get.scoop.sh | iex

Supabase をインストール

Scoop を使用して、Supabase の Bucket と Supabase をインストールします。

scoop bucket add supabase https://github.com/supabase/scoop-bucket.git
scoop install supabase

ローカル環境で Supabase を実行

ローカル環境で Supabase を実行します。別途 Docker を立ち上げておいてくださいね。

supabase start

無事に Supabase が立ち上がると下記のようにステータスが表示されると思います。

supabase local development setup is running.

         API URL: http://127.0.0.1:54321
     GraphQL URL: http://127.0.0.1:54321/graphql/v1
          DB URL: postgresql://postgres:postgres@127.0.0.1:54322/postgres
      Studio URL: http://127.0.0.1:54323
    Inbucket URL: http://127.0.0.1:54324
      JWT secret: super-secret-jwt-token-with-at-least-32-characters-long
        anon key: ********
service_role key: ********

もし自動的に表示されない場合は、

supabase status

で表示できます。

環境変数ファイルの設定

プロジェクトのルートディレクトリにある .env.local.example.env.local にリネームしてから、エディタで開いて編集します。

.env.local は下記のような内容ですが、

# Supabase Public
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=

# Supabase Private
SUPABASE_SERVICE_ROLE_KEY=

# Ollama
NEXT_PUBLIC_OLLAMA_URL=http://localhost:11434

# API Keys (Optional: Entering an API key here overrides the API keys globally for all users)
OPENAI_API_KEY=
ANTHROPIC_API_KEY=
GOOGLE_GEMINI_API_KEY=
MISTRAL_API_KEY=
PERPLEXITY_API_KEY=
OPENROUTER_API_KEY=

# OpenAI API Information
NEXT_PUBLIC_OPENAI_ORGANIZATION_ID=

# Azure API Information
AZURE_OPENAI_API_KEY=
NEXT_PUBLIC_AZURE_OPENAI_ENDPOINT=
NEXT_PUBLIC_AZURE_GPT_35_TURBO_ID=
NEXT_PUBLIC_AZURE_GPT_45_VISION_ID=
NEXT_PUBLIC_AZURE_GPT_45_TURBO_ID=

その中から、とりあえず下記だけ設定してファイルを保存しておきましょう。

# Supabase Public
NEXT_PUBLIC_SUPABASE_URL=[supabase status で表示されている "API URL" (e.g. http://127.0.0.1:54321)]
NEXT_PUBLIC_SUPABASE_ANON_KEY=[supabase status で表示されている "anon key"]

# Supabase Private
SUPABASE_SERVICE_ROLE_KEY=[supabase status で表示されている "service_role key"]

...略...

# API Keys (Optional: Entering an API key here overrides the API keys globally for all users)
OPENAI_API_KEY=[OpenAI の API Key]

...略...

# OpenAI API Information
NEXT_PUBLIC_OPENAI_ORGANIZATION_ID=[OpenAI の Organization ID (アカウント設定の画面から取得可能)]

アプリケーションの実行

次に、アプリケーションを実行します。

npm run chat

http://localhost:3000 にアプリケーションが立ち上がれば OK です。

もし、ローカル環境でしか使わないということであれば、これで終わり。立ち上げた URL からアカウントを作ったり、設定ができるので、画面の指示に従って進めていけば使えるようになります。

Vercel にホストしたい場合は、アカウント作成や設定などはせず、一旦アプリケーションを終了して次へ。

もし、ローカル環境でも使いたいし、Vercel にホストしたものも併用したいという場合、同じプロジェクトで併用はできませんので、別途、Vercel にホストする用のプロジェクトを作成し、ここまでの手順を再度実施してください。

Supabase の準備

Supabase のダッシュボードにログインし、「New project」 ボタンを押して、新規プロジェクトの作成に進みます。プロジェクト名やデータベースのパスワードは自由に決めてください。

Supabase で新規プロジェクトの作成

プロジェクトの作成が終わると、API Key などが取得できます (プロジェクトの設定画面から、「API Settings」 に進んでも確認できます)が、まずはプロジェクトの設定画面 (General) に進み、そこに表示されている 「Reference ID」 をコピーしておいてください。

ローカルデータベースのテーブル定義をリモートデータベースにコピー

先ほど、ローカル環境で一度アプリケーションを立ち上げたので、ローカル環境の Supabase には必要なテーブルが定義されています。これを、先ほど作ったクラウド上の Supabase にコピーしてあげます。

プロジェクトのルートディレクトリで、下記のコマンドを実行します。

supabase login

Enter を押せっていう指示に従うと、ブラウザで Supabase が開きますのでログインしましょう (すでにログインしていれば、そのまま完了します)。

ログインに成功したら次に下記。

supabase link --project-ref [ここにさっきコピーした Reference ID の値]

データベースのパスワードを聞かれるので、設定したものを入力して Enter すると、「リモートのプロジェクトとリンクできたよ」 というメッセージがでるので、次に下記。

supabase db push

これで、ローカルのデータベースからテーブル定義がコピーされ、反映されます。

SQL ファイルの変更

プロジェクトのルートディレクトリに supabase というディレクトリがありますが、その中の migrations ディレクトリ内に、20240108234540_setup.sql というファイルがあるので、これをエディタで開き、53行目あたりにある、下記を編集します。

project_url TEXT := 'http://supabase_kong_chatbotui:8000';
service_role_key TEXT := 'eyJhbGciOiJI...';

それぞれ、先ほど作った Supabase のプロジェクト設定画面から取得できる値に差し替えます。

project_url TEXT := '[Project URL (e.g. https://exapmle.supabase.co)]';
service_role_key TEXT := '[service_role (この情報は機密情報なので第三者に開示しないこと)]';

修正したらファイルを保存しておきます。

この状態で、一旦、GitHub なり、Vercel から接続可能なリモートリポジトリにコードを push しておきましょう。

Vercel の設定

Vercel で新規プロジェクトを作成します。プロジェクトを読み込むと、その流れの中で環境変数 (Environment Variables) が設定できますので、下記のように、KeyValue の組み合わせを設定しましょう。

Key Value
NEXT_PUBLIC_SUPABASE_URL Project URL
NEXT_PUBLIC_SUPABASE_ANON_KEY Project API keys にある「anon」
SUPABASE_SERVICE_ROLE_KEY Project API keys にある「service_role」
OPENAI_API_KEY OpenAI の API Key
NEXT_PUBLIC_OPENAI_ORGANIZATION_ID OpenAI の Organization ID

各環境変数を設定したら、デプロイします。

Supabase で重要な設定

で、この次が重要 (Chatbot UI の README.md に書かれてない) なんですが、Vercel でデプロイしたら、ドメイン (Domains) 設定から、本番環境の URL をコピーして、一旦、Supabase の方に移動します。

プロジェクトのメニューから 「Authentication」 → 「URL Configuration」 と進むと、「Site URL」 という部分に、初期設定で localhost が入っていると思いますが、ここに、先ほど Vercel からコピーした、URL を入れて保存しておきます (Vercel でカスタムドメインを設定した場合はそのドメイン)。

Supabase プロジェクトの URL 設定画面

Chatbot UI の設定

やっとここまできた。最後に Vercel にデプロイした Chatbot UI にアクセスし、アカウントを作ったりしていきます。

初めてアクセスすると、下記のようなログイン画面が出ると思いますので、メールアドレスと、パスワードを入力して、「Sign Up」 の方をクリックしましょう。すると、入力したメールアドレス宛に、「Confirm Your Signup」 というメールが、Supabase から届くと思いますので、「Confirm your mail」 リンクをクリックしてメールアドレスを確認します。

Chatbot UI のログイン画面

そうすると、アカウントが登録されると思いますので、今度はログインします。無事ログインできると、初期設定画面が表示されると思いますので、あとは指示に従って進めていってください。完了すると、Chatbot UI が利用可能になります。

本家 ChatGPT のように、チャットに質問などを入力すれば API を介して返答が返ってきます。当然、API の利用料金が送ったプロンプトの文字数に応じてかかりますので、OpenAI の管理画面から確認しておきましょう。利用金額のリミットなども設定できますので、設定しておくといいでしょう。

ベーシック認証をかける

Chatbot UI は、そのままだと自由にアカウントを追加できてしまうので、それが困るという場合は、Supabase のプロジェクト設定 → 「Authentication」 の項目内、「User Signups」 のところに 「Allow new users to sign up」 という項目が初期設定だと有効になっていると思いますので、これを無効にしておくとよいでしょう。

当たり前ですが、この設定が無効だと、新規のユーザー登録が不可になりますから、自分のアカウントを作る前に無効にしないようにしてください。

また、第三者にログイン画面にアクセスされるのも嫌だなという場合に備えて、下記にベーシック認証を追加する方法を最後に。

方法はいくつかあると思いますが、今回は Next.js の Middleware を使用して簡単に実装することにしました。

もともと、プロジェクトルートに、middleware.ts がありますから、これを改変してしまいます。

まず、Vercel の環境変数に、下記を追加しておきます。

Key Value
AUTHORIZATION_ID ベーシック認証のユーザー名
AUTHORIZATION_PASS ベーシック認証のパスワード

その上で、プロジェクトルートにある、middleware.ts を下記のように変更してリポジトリに push しましょう。

import { createClient } from "@/lib/supabase/middleware";
import { NextResponse, type NextRequest } from "next/server";

export async function middleware(request: NextRequest) {
  // ベーシック認証を実行
  const authorizationHeader = request.headers.get('authorization');
  const validAuth = validateBasicAuth(authorizationHeader);

  if (!validAuth) {
    return new Response('Authorization required', {
      status: 401,
      headers: {
        'WWW-Authenticate': 'Basic realm="Secure Area"',
      },
    });
  }
  // ベーシック認証を通ったら、元ファイルの処理
  try {
    const { supabase, response } = createClient(request);

    const session = await supabase.auth.getSession();

    const redirectToChat = session && request.nextUrl.pathname === "/";

    if (redirectToChat) {
      return NextResponse.redirect(new URL("/chat", request.url));
    }

    return response;
  } catch (e) {
    return NextResponse.next({
      request: {
        headers: request.headers
      }
    });
  }
}
// ベーシック認証
function validateBasicAuth(authorizationHeader: string | null) {
  if (!authorizationHeader) {
    return false;
  }

  const base64Credentials = authorizationHeader.split(' ')[1];
  const credentials = Buffer.from(base64Credentials, 'base64').toString('ascii');
  const [id, password] = credentials.split(':');

  return (
    id === process.env.AUTHORIZATION_ID && password === process.env.AUTHORIZATION_PASS
  );
}

上記ソースコードは本記事執筆時点での middleware.ts をベースにしていますので、タイミングによっては上記と異なる場合がありますので注意してください。

細かい修正

下記、3つの修正は記事書いた後で本家リポジトリに PR 出していたのが取り込まれましたので、最新のデータを使えばすでに反映されています。

細かいんですけども、現状のソースコードだと、各ページに title 要素が入ってないんですよね。なので、下記のように 2つのファイルを修正します。

1つめは、app/layout.tsx ですが、

import { Metadata } from "next" // ←この行を追加
import { Toaster } from "@/components/ui/sonner"
import { GlobalState } from "@/components/utility/global-state"
import { Providers } from "@/components/utility/providers"
import { Database } from "@/supabase/types"
import { createServerClient } from "@supabase/ssr"
import { Inter } from "next/font/google"
import { cookies } from "next/headers"
import { ReactNode } from "react"
import "./globals.css"

const inter = Inter({ subsets: ["latin"] })

interface RootLayoutProps {
  children: ReactNode
}

// ↓このブロックを追加
export const metadata: Metadata = {
  title: {
    template: "%s - Chatbot UI",
    default: "Chatbot UI",
  },
}

export default async function RootLayout({ children }: RootLayoutProps) {
...略...
}

もう 1は、app/login/page.tsx ですが、

import { Metadata } from "next" // ←この行を追加
import { Brand } from "@/components/ui/brand"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { createClient } from "@/lib/supabase/server"
import { Database } from "@/supabase/types"
import { createServerClient } from "@supabase/ssr"
import { cookies, headers } from "next/headers"
import { redirect } from "next/navigation"

// ↓このブロックを追加
export const metadata: Metadata = {
  title: "Log in",
}

これで、ログインページには <title>Log in - Chatbot UI</title> が、その他のページには <title>Chatbot UI</title> が出力されますので、タイトルなしページにはならないと思います。

また、public ディレクトリ直下に favicon.ico がないため、コンソールに、404 Not Found が出続けてると思いますが、気になる人は favicon.ico を作って置いてあげるといいと思います。

関連エントリー

記事をここまで御覧頂きありがとうございます。
この記事が気に入ったらサポートしてみませんか?