🐥
Vercel AI SDK の Quickstart で AI Chatボットアプリを構築(Server Actionsを利用)
はじめに
前回の記事で Vercel AI SDK の Quickstart を参考に AI Chat ボットアプリを構築しました。今回は、Route Handler ではなく、Server Actions を利用して実装します。
こちらの内容を参考にしながら Server Actions 化を進めます。
リポジトリをクローン
こちらのリポジトリーをクローンします。
Server Actionを作成
Server Action を作成します。
$ mkdir -p src/app/actions/
$ touch src/app/actions/conversation.ts
$ touch src/app/actions/conversation-stream.ts
src/app/actions/conversation.ts
'use server';
import { createStreamableValue } from 'ai/rsc';
import { CoreMessage, streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
export async function continueConversation(messages: CoreMessage[]) {
const result = await streamText({
model: openai('gpt-3.5-turbo'),
messages,
});
const stream = createStreamableValue(result.textStream);
return stream.value;
}
src/app/actions/conversation-stream.ts
'use server';
import { createStreamableValue } from 'ai/rsc';
import { CoreMessage, streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
export async function continueConversation(messages: CoreMessage[]) {
'use server';
const result = await streamText({
model: openai('gpt-4-turbo'),
messages,
});
const data = { test: 'hello' };
const stream = createStreamableValue(result.textStream);
return { message: stream.value, data };
}
UIを更新
UI を更新します。
src/app/components/chat.tsx
"use client";
import { useState, type FC } from "react";
import { continueConversation } from "../actions/conversation";
import { CoreMessage } from "ai";
import { readStreamableValue } from "ai/rsc";
type ChatProps = {};
export const Chat: FC<ChatProps> = ({}) => {
const [messages, setMessages] = useState<CoreMessage[]>([]);
const [input, setInput] = useState("");
return (
<div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
{messages.map((m, i) => (
<div key={i} className="whitespace-pre-wrap">
{m.role === "user" ? "User: " : "AI: "}
{m.content as string}
</div>
))}
<form
action={async () => {
const newMessages: CoreMessage[] = [
...messages,
{ content: input, role: "user" },
];
setMessages(newMessages);
setInput("");
const result = await continueConversation(newMessages);
for await (const content of readStreamableValue(result)) {
setMessages([
...newMessages,
{
role: "assistant",
content: content as string,
},
]);
}
}}
>
<input
className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl"
value={input}
placeholder="Say something..."
onChange={(e) => setInput(e.target.value)}
/>
</form>
</div>
);
};
コミットします。
$ git add .
$ git commit -m "feat: add server actions"
UIを更新
UI を更新します。
src/app/components/chat.tsx
"use client";
import { useState, type FC } from "react";
import { continueConversation } from "../actions/conversation";
import { CoreMessage } from "ai";
import { readStreamableValue } from "ai/rsc";
type ChatProps = {};
export const Chat: FC<ChatProps> = ({}) => {
const [messages, setMessages] = useState<CoreMessage[]>([]);
const [input, setInput] = useState("");
return (
<div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
{messages.map((m, i) => (
<div key={i} className="whitespace-pre-wrap">
{m.role === "user" ? "User: " : "AI: "}
{m.content as string}
</div>
))}
<form
action={async () => {
const newMessages: CoreMessage[] = [
...messages,
{ content: input, role: "user" },
];
setMessages(newMessages);
setInput("");
const result = await continueConversation(newMessages);
for await (const content of readStreamableValue(result)) {
setMessages([
...newMessages,
{
role: "assistant",
content: content as string,
},
]);
}
}}
>
<input
className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl"
value={input}
placeholder="Say something..."
onChange={(e) => setInput(e.target.value)}
/>
</form>
</div>
);
};
src/app/components/chat-stream.tsx
"use client";
import { useState, type FC } from "react";
import { continueConversation } from "../actions/conversation-stream";
import { CoreMessage } from "ai";
import { readStreamableValue } from "ai/rsc";
type ChatProps = {};
export const Chat: FC<ChatProps> = ({}) => {
const [messages, setMessages] = useState<CoreMessage[]>([]);
const [input, setInput] = useState("");
const [data, setData] = useState<any>();
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, i) => (
<div key={i} className="whitespace-pre-wrap">
{m.role === "user" ? "User: " : "AI: "}
{m.content as string}
</div>
))}
<form
action={async () => {
const newMessages: CoreMessage[] = [
...messages,
{ content: input, role: "user" },
];
setMessages(newMessages);
setInput("");
const result = await continueConversation(newMessages);
setData(result.data);
for await (const content of readStreamableValue(result.message)) {
setMessages([
...newMessages,
{
role: "assistant",
content: content as string,
},
]);
}
}}
>
<input
className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl"
value={input}
placeholder="Say something..."
onChange={(e) => setInput(e.target.value)}
/>
</form>
</div>
);
};
アプリケーションを実行
作成したアプリケーションの動作を確認します。
$ pnpm run dev
ブラウザで http://localhost:3000/ にアクセスします。
ブラウザで http://localhost:3000/stream にアクセスします。
さいごに
前回の作業リポジトリに対して Server Actions 追加しました。
作業リポジトリはこちらです。
Discussion