Vercel AI SDKのGenerative UI with RSCを試してみた
この記事は株式会社ガラパゴス(有志)アドベントカレンダー2024の16日目の記事です。
About
本日は、Vercel AI SDKのGenerative UI with RSCを動かしてみた時のメモを共有します。
そもそもVercel AI SDKとは?については別途紹介記事などがあるので割愛しますが、Vercel AI SDKの主要な機能の1つである、Generative UI with RSC
に興味を惹かれたので、実際に動かしてみました。
基本的に公式サイトの内容をベースにしています。
セットアップ
まずは、公式で公開されているサンプルNextアプリケーションを作成します。
npx create-next-app --example https://github.com/vercel-labs/ai-sdk-preview-rsc-genui ai-sdk-preview-rsc-genui-example
作成後、環境変数に利用するLLMのAPI Keyを設定します。ここではOpenAIを利用していますが、利用するLLMに応じて変えて下さい。
cd ai-sdk-preview-rsc-genui-example/
cp .env.example .env
vim .env
OPENAI_API_KEY=<OpenAIのAPI Key>
npm run dev
します。
npm install
npm run dev
すると、 http://localhost:3000
/ でアプリケーションが起動するので、アクセスして画面が表示されればOK。
とりあえず動かしてみる
Generative UI with RSC の中身を全く理解していない状態ですが、とりあえずアプリケーションのメッセージに↓の文章を投入してみました。
投入した文章 (500文字程度)
次の文章を、分かりやすい見せ方で表現して下さい。
私は3つのご飯が好きです。1つは焼き肉、2つ目は寿司、3つ目はカレーです。これらはいずれも異なる味わいと食感を持っており、その日の気分や季節によって選び分けることができます。例えば、暑い夏の日には、さっぱりとした寿司をつまみながら冷たい麦茶を飲むと、心地よい涼しさが体中に広がります。一方で、寒い冬にはスパイスのきいたカレーを頬張ると、体の芯から温まり、ほっとした気持ちになります。そして、友人たちと集まってワイワイ楽しむ焼き肉は、煙の香ばしさと肉汁が口の中ではじけ、思わず笑顔がこぼれます。 これらの料理は、単なる食事以上の意味を持っています。寿司屋のカウンター越しに握られた一貫は、子供の頃に父と通った思い出を呼び覚まし、焼き肉を囲んだ深夜の語らいは、学生時代の仲間たちとの固い絆を思い出させます。また、カレーの豊かな香りに包まれると、旅先で出会った異国の味や文化が脳裏に甦り、遠い土地と今の自分をつなぐ不思議な懐かしさを感じます。 こうした3つのご飯は、私の人生の断片を彩る大切な存在なのです。食べる度に過去と現在、そして未来がつながり、美味しさと共に心の中に豊かな物語を紡いでいくのかもしれません。
あれ?これはただのText(Markdown)出力では。。
Generative UI with RSC の機能を、状況に応じて多種多様なUIをその場で生成してくれる、v0ライクにものだと勝手に期待していたのですが、どうも違うようでした。
(そりゃそうだ)
中身を覗いてみる
そこで、公式ドキュメントとアプリケーションをソースコードをもう少し覗いてみます。
公式ドキュメントに記載のあったサンプルコードで、世界観を理解しました。
const result = await streamUI({
model: openai('gpt-4o'),
prompt: 'Get the weather for San Francisco',
text: ({ content }) => <div>{content}</div>,
tools: {
getWeather: {
description: 'Get the weather for a location',
parameters: z.object({ location: z.string() }),
generate: async function* ({ location }) {
yield <LoadingComponent />;
const weather = await getWeather(location);
return <WeatherComponent weather={weather} location={location} />;
},
},
},
});
どうやら、Function Calling(Tool Calling)
の枠組みの中で動く仕組みのようです。
推論の過程で特定のToolの利用が妥当であると判断した場合にToolを参照し、紐づけられたUIコンポーネントをクライアント側に返す仕組みのようです。
ここから読み取れる点として、次の2点が挙げられます。
- Function Callingが前提
- 事前に用意されたUIコンポーネントが前提
1点目に関しては、Function Calling未対応のo1-preview(o1)では動かないだろうな、と思って試してみましたが、予想通り動きませんでした。
2点目に関しては、これだけでも十分魅力的なアプリケーションを作れそうな感触もありますが、やはりコンポーネントを揃えて管理していくコストを考えると、もう少し動的な仕組みにならないか?と期待したいところです。
動的にコンポーネントを生成して返せないか?
そこで、動的にコンポーネントを生成して返せないか?、という問いに対して、React(Next.js)素人の自分なりに試してみました。
(が、すぐに諦めましたので、そのログだけ公開します。)
具体的には、
- LLMにReactコンポーネントの文字列(JSX準拠)を生成させる
- 生成した文字列をParseしてコンポーネントのオブジェクトに変換し、クライアントに返す
というシンプルなアプローチを試してみました。
react-jsx-parser の導入
文字列からReactコンポーネントに変換するライブラリをさがしたところ、react-jsx-parserなるものを発見しました。
他にもあったのですが、Star数を加味してもっとも安定してそうだったので選択しました。
npm install
ですぐに使えるようになります。
npm install --save react-jsx-parser
実装してみる
サンプルアプリケーションでは、RSCコンテキストでのstreamUIの処理がapp/(preview)/actions.tsx
に実装されています。
その中で、tools
の定義に1件追加しました。要約すべきテキストを受取り、LLMを使ってHTML化、そのままJSXとして解釈させてコンポーネント化する流れです。
tools: {
viewTextSummary: {
description: "view text summary",
parameters: z.object({
// ツールとして整理すべき文章をtext変数で受け取る
text: z.string(),
}),
generate: async function* ({ text }) {
// LLMを使ってコンポーネントのJSXを記述させる
const prompt = `
下記の文章を、端的に3点で整理して表示するためのHTMLを実装して下さい。
・bodyタグの内側に記述するタグのみ出力して下さい
・外部のCSSやJavaScriptは使わない前提で実装して下さい
・とはいえ、味気ない見た目にならないように、できる限り分かりやすいビジュアルを目指して下さい
<文章>
${text}
`;
const { text: jsx } = await generateText({
model: openai("gpt-4o"),
prompt,
});
// LLMが生成したJSXをコンポーネント化
const DynamicComponent = () => (
<JsxParser
bindings={{}}
components={{}}
jsx={jsx}
/>
);
return <Message role="assistant" content={<DynamicComponent></DynamicComponent>} />;
},
},
この状態で npm run dev
してみたところ、次のようなエラーが。。
どうやら、react-jsx-parser自体が、use client
的な世界で使うことを前提とした造りになっているようで、RSCのコンテキストでそのまま使うことは難しそうでした。
(react-jsx-parser以外のライブラリも試してみましたが、同様)
もう少し頑張れば出来そうな気がしましたが、時間切れでこれまで。
まとめ
自分は普段、v0を使ってフロントの開発を進めていますが、「このv0の機能性が、エンドユーザー側にもそのまま持っていけたらなー」と感じていました。
Vercel AI SDKのGenerative UI with RSCは、その世界観に向けた最初の1歩になるSDKだと思いますが、現状では固定のコンポーネントを返す世界観であるため、ある程度フロントエンドの実装側で頑張る必要がありそうです。
今後もアップデートに注目していきたいと思います。
Discussion