📘

Vercel AI SDK の Quickstart で AI Chatボットアプリを構築

2024/05/17に公開

はじめに

Vercel AI SDK にある Next.js App Router Quickstart を試して AI Chat ボットアプリを構築します。内容は公式から少し変更しています。

https://sdk.vercel.ai/docs/getting-started/nextjs-app-router

前提条件

こちらが前提条件です。

  • Node.js 18+
  • OpenAI API キー

動作確認の環境を構築

動作確認するための Next.js プロジェクトを作成します。長いので、折り畳んでおきます。

新規プロジェクト作成と初期環境構築の手順詳細

プロジェクトの作成

create next-app@latestでプロジェクトを作成します。

$ pnpm create next-app@latest next-vercel-ai-sample --typescript --eslint --import-alias "@/*" --src-dir --use-pnpm --tailwind --app
$ cd next-vercel-ai-sample 

不要な設定を削除し、プロジェクトを初期化します。

styles

CSSなどを管理するstylesディレクトリを作成します。globals.cssを移動します。

$ mkdir -p src/styles
$ mv src/app/globals.css src/styles/globals.css

globals.cssの内容を以下のように上書きします。

src/styles/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;

初期ページ

app/page.tsxを上書きします。

src/app/page.tsx
import { type FC } from "react";

const Home: FC = () => {
  return (
    <div className="">
      <div className="text-lg font-bold">Home</div>
      <div>
        <span className="text-blue-500">Hello</span>
        <span className="text-red-500">World</span>
      </div>
    </div>
  );
};

export default Home;

レイアウト

app/layout.tsxを上書きします。

src/app/layout.tsx
import "@/styles/globals.css";
import { type FC } from "react";
type RootLayoutProps = {
  children: React.ReactNode;
};

export const metadata = {
  title: "Sample",
  description: "Generated by create next app",
};

const RootLayout: FC<RootLayoutProps> = (props) => {
  return (
    <html lang="ja">
      <body className="">{props.children}</body>
    </html>
  );
};

export default RootLayout;

TailwindCSSの設定

TailwindCSSの設定を上書きします。

tailwind.config.ts
import type { Config } from 'tailwindcss'

const config: Config = {
  content: [
    './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
    './src/components/**/*.{js,ts,jsx,tsx,mdx}',
    './src/app/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  plugins: [],
}
export default config

TypeScriptの設定

TypeScriptの設定を上書きします。

tsconfig.json
{
  "compilerOptions": {
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "baseUrl": ".",
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

スクリプトを追加

型チェックのスクリプトを追加します。

package.json
{
  "scripts": {
+   "typecheck": "tsc"
  },
}

動作確認

ローカルで動作確認します。

$ pnpm run dev

コミットして作業結果を保存しておきます。

$ git add .
$ git commit -m "feat:新規にプロジェクトを作成し, 作業環境を構築"

依存関係のインストール

ai@ai-sdk/openai、タイプバリエーションのパッケージをインストールします。

$ pnpm install ai @ai-sdk/openai zod

ai は Vercel AI のパッケージです。

https://github.com/vercel/ai

@ai-sdk/openai は OpenAI の API を利用するための Vercel AI SDK のライブラリです。

https://sdk.vercel.ai/providers/ai-sdk-providers/openai

コミットします。

$ git add .
$ git commit -m "feat: Install dependencies"

OpenAI API を取得

OpenAI API キーの取得方法はこちらを参照してください。

https://zenn.dev/hayato94087/articles/85378e1f7bc0e5#openai-の-apiキーの取得

OpenAI API キーの設定

.env.local ファイルを作成します。

$ touch .env.local

.env に OpenAI の API キーを設定します。

.env
OPENAI_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxx

xxxxxxxxxxxxxxxxxxxxxxxx 実際の OpenAI API キーに置き換えてください。

Chat アプリの作成

ここでは Chat アプリを作成していきます。

Route Handler を作成

API を呼び出せるように Route Handler を作成します。

src/app/api/chat/route.ts を作成します。

$ mkdir -p src/app/api/chat
$ touch src/app/api/chat/route.ts
src/app/api/chat/route.ts
import { openai } from '@ai-sdk/openai';
import { StreamingTextResponse, streamText } from 'ai';

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = await streamText({
    model: openai('gpt-3.5-turbo'),
    messages,
  });

  return new StreamingTextResponse(result.toAIStream());
}

コードについて解説します。

下記では、非同期関数POSTリクエストを定義します。この関数では、messagesをリクエストボディから取得します。messagesにはこれまでのチャットボットとの会話の履歴が含まれます。過去の会話の履歴を元に、AIの返答を生成します。

export async function POST(req: Request) {
  const { messages } = await req.json();
  ...
}

下記では、streamText関数を呼び出して、AI の返答を生成します。streamTextai パッケージに含まれる関数です。streamText 関数は、modelmessages を引数に取ります。model は AI のモデルを指定します。messages は過去の会話の履歴を指定します。streamText には追加で設定も指定できます。

import { StreamingTextResponse, streamText } from 'ai';

export async function POST(req: Request) {
  ...
  const result = await streamText({
    model: openai('gpt-4-turbo'),
    messages,
  });
  ...
}

下記では、stream 関数はStreamTextResultを返します。StreamTextResulttoAIStream関数を持ちます。toAIStream 関数は、StreamTextResultStreamingTextResponseで扱えるように変換します。クライアントに StreamingTextResponse を返します。

  return new StreamingTextResponse(result.toAIStream());

コミットします。

$ git add .
$ git commit -m "feat: Add route handler"

これで、Route Handler を通して LLM にクエリを送信する準備が整いました。次は利用するためのフロントエンドを作成します。

UI を作成

Vercel SDK UI ライブラリーを利用することで、チャットインターフェースの実装を簡素化できます。

コンポーネントを作成します。

$ mkdir -p src/app/components
$ touch src/app/components/chat.tsx
src/app/components/chat.tsx
"use client";

import { type FC } from "react";
import { useChat } from "ai/react";

type ChatProps = {};

export const Chat: FC<ChatProps> = ({}) => {
  const { messages, input, handleInputChange, handleSubmit } = useChat();
  return (
    <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
      {messages.map((m) => (
        <div key={m.id} className="whitespace-pre-wrap">
          {m.role === "user" ? "User: " : "AI: "}
          {m.content}
        </div>
      ))}

      <form onSubmit={handleSubmit}>
        <input
          className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl dark:text-black"
          value={input}
          placeholder="Say something..."
          onChange={handleInputChange}
        />
      </form>
    </div>
  );
};

useChat はフックです。useChat は、チャットメッセージを管理するための状態と関数を返します。useChat はデフォルトで /api/chat エンドポイントに POST リクエストを送信します。useChat は、messagesinputhandleInputChangehandleSubmit, isLoading を返します。

項目 説明
messages チャットメッセージの配列です。メッセージオブジェクトには id, rolecontent を含みます。
input ユーザーが入力したテキストです。
handleInputChange ユーザーが入力したテキストを更新するための関数です。
handleSubmit フォームが送信されたときに呼び出される関数です。
isLoading リクエストが送信されているかどうかを示す真偽値です。

useChat の詳細はこちらです。

https://sdk.vercel.ai/docs/reference/ai-sdk-ui/use-chat

app/page.tsx を更新し、チャットメッセージを表示できるようにします。

src/app/page.tsx
import { type FC } from "react";
import { Chat } from "@/app/components/chat";

type HomeProps = {};

const Page: FC<HomeProps> = ({}) => {
  return (
    <>
      <Chat />
    </>
  );
};

export default Page;

コミットします。

$ git add .
$ git commit -m "feat: Add Chat component and integrate with useChat hook"

アプリケーションを実行

作成したアプリケーションの動作を確認します。

$ pnpm run dev

ブラウザで http://localhost:3000 にアクセスします。

https://twitter.com/hayato94087/status/1791311428521247085

ストリーミング対応

ここでは先程作成したアプリケーションに、データをストリーミングする機能を追加します。

Route Handler を作成

ユースケースによっては、AI の応答とともにデータをストリーミングすることが必要になる場合があります。StreamData を利用することで実現できます。

新規に Route を作成します。

$ mkdir -p src/app/api/chat/stream
$ touch src/app/api/chat/stream/route.ts
src/app/api/chat/stream/route.ts
import { openai } from '@ai-sdk/openai';
import { StreamingTextResponse, streamText, StreamData } from 'ai';

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = await streamText({
    model: openai('gpt-3.5-turbo'),
    messages,
  });

  const data = new StreamData();

  data.append({ test: 'value' });

  const stream = result.toAIStream({
    onFinal(_) {
      data.close();
    },
  });

  return new StreamingTextResponse(stream, {}, data);
}

コードの説明をします。


前半部分は src/app/api/chat/route.ts と同じです。違いについて説明します。

下記では、StreamData のインスタンスを作成します。

  const data = new StreamData();

下記では、ストリームに配信したいデータをアペンドします。ここではダミーデータを送るようにしています。

  data.append({ test: 'value' });

下記では、StreamTextResult オブジェクトの toAIStream メソッドを使用して新しい AI ストリームを作成します。

  const stream = result.toAIStream({

下記では作成した AI ストリームの onFinal コールバックをリッスンします。onFinal コールバックはストリームが終了したときに呼び出されます。data.close() を呼び出すことで、ストリームが閉じられます。

    onFinal(_) {
      data.close();
    },

追加したデータとストリームを新しい StreamingTextResponse に渡します。

  return new StreamingTextResponse(stream, {}, data);

コミットします。

$ git add .
$ git commit -m "feat: Add route for streaming chat messages"

UIを作成

ストリームデータを受信するための UI を作成します。新しいパスを作成します。

新規にコンポーネントを作成します。

$ touch src/app/components/chat-stream.tsx
src/app/components/chat-stream.tsx
"use client";

import { type FC } from "react";
import { useChat } from "ai/react";

type ChatProps = {};

export const ChatStream: FC<ChatProps> = ({}) => {
  const { messages, input, handleInputChange, handleSubmit, data } = useChat({
    api: "/api/chat/stream",
  });
  return (
    <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
      {data && <pre>{JSON.stringify(data, null, 2)}</pre>}
      {messages.map((m) => (
        <div key={m.id} className="whitespace-pre-wrap">
          {m.role === "user" ? "User: " : "AI: "}
          {m.content}
        </div>
      ))}

      <form onSubmit={handleSubmit}>
        <input
          className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl dark:text-black"
          value={input}
          placeholder="Say something..."
          onChange={handleInputChange}
        />
      </form>
    </div>
  );
};

コードをの説明をします。

useChatからdataを取得します。dataはストリームデータを含みます。

  const { messages, input, handleInputChange, handleSubmit, data } = useChat({
    api: "/chat/stream",
  });

data が存在する場合、pre タグを使用してデータを表示します。

      {data && <pre>{JSON.stringify(data, null, 2)}</pre>}

新規にページを作成します。

$ mkdir -p src/app/stream
$ touch src/app/stream/page.tsx
src/app/stream/page.tsx
import { type FC } from "react";
import { ChatStream } from "@/app/components/chat-stream";

interface HomeProps {}

const Page: FC<HomeProps> = ({}) => {
  return (
    <>
      <ChatStream />
    </>
  );
};

export default Page;

コミットします。

$ git add .
$ git commit -m "feat: Add ChatStream component and integrate with useChat hook"

アプリケーションを実行

作成したアプリケーションの動作を確認します。

$ pnpm run dev

ブラウザで http://localhost:3000/stream にアクセスします。

https://x.com/hayato94087/status/1791569829352390868

以下のようにダミーデータが先頭に見えていれば成功です。

alt text

さいごに

この記事では、Vercel AI SDK の Quickstart を試して AI Chat ボットアプリを構築しました。

作業リポジトリはこちらです。

https://github.com/hayato94087/next-vercel-ai-sample

Discussion