Vercel AI SDK の Quickstart で AI Chatボットアプリを構築
はじめに
Vercel AI SDK にある Next.js App Router Quickstart を試して AI Chat ボットアプリを構築します。内容は公式から少し変更しています。
前提条件
こちらが前提条件です。
- 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
の内容を以下のように上書きします。
@tailwind base;
@tailwind components;
@tailwind utilities;
初期ページ
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
を上書きします。
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の設定を上書きします。
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の設定を上書きします。
{
"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"]
}
スクリプトを追加
型チェックのスクリプトを追加します。
{
"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 のパッケージです。
@ai-sdk/openai
は OpenAI の API を利用するための Vercel AI SDK のライブラリです。
コミットします。
$ git add .
$ git commit -m "feat: Install dependencies"
OpenAI API を取得
OpenAI API キーの取得方法はこちらを参照してください。
OpenAI API キーの設定
.env.local
ファイルを作成します。
$ touch .env.local
.env
に OpenAI の API キーを設定します。
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
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 の返答を生成します。streamText
は ai
パッケージに含まれる関数です。streamText
関数は、model
と messages
を引数に取ります。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
を返します。StreamTextResult
はtoAIStream
関数を持ちます。toAIStream
関数は、StreamTextResult
をStreamingTextResponse
で扱えるように変換します。クライアントに 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
"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
は、messages
、input
、handleInputChange
、handleSubmit
, isLoading
を返します。
項目 | 説明 |
---|---|
messages |
チャットメッセージの配列です。メッセージオブジェクトには id , role と content を含みます。 |
input |
ユーザーが入力したテキストです。 |
handleInputChange |
ユーザーが入力したテキストを更新するための関数です。 |
handleSubmit |
フォームが送信されたときに呼び出される関数です。 |
isLoading |
リクエストが送信されているかどうかを示す真偽値です。 |
useChat
の詳細はこちらです。
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 にアクセスします。
ストリーミング対応
ここでは先程作成したアプリケーションに、データをストリーミングする機能を追加します。
Route Handler を作成
ユースケースによっては、AI の応答とともにデータをストリーミングすることが必要になる場合があります。StreamData
を利用することで実現できます。
新規に Route を作成します。
$ mkdir -p src/app/api/chat/stream
$ touch 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
"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
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 にアクセスします。
以下のようにダミーデータが先頭に見えていれば成功です。
さいごに
この記事では、Vercel AI SDK の Quickstart を試して AI Chat ボットアプリを構築しました。
作業リポジトリはこちらです。
Discussion