kk-web

Next.js×AIの最新の書きっぷり

2024-10-02

日々 AI を用いた書きっぷりはどんどん変わっていまして。

基本的にアメリカを中心に情報がアップデートされているため、日本語の情報はかなり後手後手な印象を受けます。

そんな中でいろいろと情報を調べてみたのですが、基本的には AI SDK を追いかけていれば問題ないかなーという印象です。

ということで、今回は Next.js × AI の最新の書きっぷりをざっくり説明していこうと思います。


前提

.env.localOPENAI_API_KEY を設定してください。

@ai-sdk/openai では環境変数の OPENAI_API_KEY を自動的に読み込んでくれるようになったみたいです。

Route Handler

app/api/chat/route.ts

import { openai } from "@ai-sdk/openai";
import { streamText, convertToCoreMessages, StreamData } from "ai";

export const maxDuration = 30;

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

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

  const result = await streamText({
    model: openai("gpt-3.5-turbo"),
    messages: convertToCoreMessages(messages),
    onFinish() {
      data.close();
    },
  });

  return result.toDataStreamResponse({ data });
}

app/page.tsx

"use client";
import { useChat } from "ai/react";

export default function Chat() {
  const { messages, input, handleInputChange, handleSubmit, data } = useChat();

  return (
    <div>
      {data && <pre>{JSON.stringify(data, null, 2)}</pre>}
      {messages.map((m) => (
        <div key={m.id}>
          {m.role === "user" ? "User: " : "AI: "}
          {m.content}
        </div>
      ))}
      <form onSubmit={handleSubmit}>
        <input
          value={input}
          placeholder="Say something..."
          onChange={handleInputChange}
        />
      </form>
    </div>
  );
}

useChat を使用すると自動的に /api/chat のエンドポイントに POST を叩くようです。

もちろんエンドポイントは動的に書き換えられるのですが、初期値があるというのもなかなか初見殺しだなと。

Server Action

app/actions.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 data = { test: "hello" };
  const stream = createStreamableValue(result.textStream);

  return { message: stream.value, data };
}

app/page.tsx

"use client";
import { type CoreMessage } from "ai";
import { useState } from "react";
import { continueConversation } from "./actions";
import { readStreamableValue } from "ai/rsc";

export const maxDuration = 30;

export default function Chat() {
  const [messages, setMessages] = useState<CoreMessage[]>([]);
  const [input, setInput] = useState("");
  const [data, setData] = useState<any>();

  return (
    <div>
      {data && <pre>{JSON.stringify(data, null, 2)}</pre>}
      {messages.map((m, i) => (
        <div key={i}>
          {m.role === "user" ? "User: " : "AI: "}
          {m.content as string}
        </div>
      ))}
      <form
        onSubmit={async (e) => {
          e.preventDefault();
          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
          value={input}
          placeholder="Say something..."
          onChange={(e) => setInput(e.target.value)}
        />
      </form>
    </div>
  );
}

サーバーアクションを使用した場合は useChat が使用できないっぽいので、結構ベタな実装感になるみたいです。

Streaming React Components

今度は打って変わって、最近微妙に流行りつつあるサーバーアクションがコンポーネントを返すサンプルです。

app/actions.tsx

"use server";

import { streamUI } from "ai/rsc";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";

const LoadingComponent = () => <div>getting weather...</div>;

const getWeather = async (location: string) => {
  await new Promise((resolve) => setTimeout(resolve, 2000));

  return "82°F️ ☀️";
};

interface WeatherProps {
  location: string;
  weather: string;
}

const WeatherComponent = (props: WeatherProps) => (
  <div>
    The weather in {props.location} is {props.weather}
  </div>
);

export async function streamComponent() {
  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} />;
        },
      },
    },
  });

  return result.value;
}

app/page.tsx

"use client";

import { useState } from "react";
import { streamComponent } from "./actions";

export default function Page() {
  const [component, setComponent] = useState<React.ReactNode>();

  return (
    <div>
      <form
        onSubmit={async (e) => {
          e.preventDefault();

          setComponent(await streamComponent());
        }}
      >
        <button>Stream Component</button>
      </form>
      <div>{component}</div>
    </div>
  );
}

サーバーアクションがコンポーネントを返すケースはまだかなりマイナーな気がしますが、こういうのもできるっぽいです。

しかしジェネレーター関数を使うとは。


ほとんど公式のコピペですが、そんな感じっぽいです。

ほかにも generateObject()streamObject() あたりもよく使用すると思いますか、こちらは公式ドキュメントを読めばほぼ問題なく理解できるかなと。

© 2018 kk-web