생성형 사용자 인터페이스(Generative UI)를 사용하면 에이전트가 텍스트를 넘어 풍부한 사용자 인터페이스를 생성할 수 있습니다. 이를 통해 대화 흐름과 AI 응답에 따라 UI가 적응하는 더욱 인터랙티브하고 컨텍스트를 인식하는 애플리케이션을 만들 수 있습니다. 예약/숙박에 대한 프롬프트와 인라인으로 UI 컴포넌트로 렌더링된 호텔 목록 카드(이미지, 제목, 가격, 위치)가 생성된 Agent Chat LangSmith는 React 컴포넌트를 graph 코드와 함께 배치할 수 있도록 지원합니다. 이를 통해 graph를 위한 특정 UI 컴포넌트를 구축하는 데 집중하면서 Agent Chat과 같은 기존 채팅 인터페이스에 쉽게 연결하고 실제로 필요할 때만 코드를 로드할 수 있습니다.

Tutorial

1. UI 컴포넌트 정의 및 구성

먼저, 첫 번째 UI 컴포넌트를 생성합니다. 각 컴포넌트에 대해 graph 코드에서 컴포넌트를 참조하는 데 사용될 고유 식별자를 제공해야 합니다.
src/agent/ui.tsx
const WeatherComponent = (props: { city: string }) => {
  return <div>Weather for {props.city}</div>;
};

export default {
  weather: WeatherComponent,
};
다음으로, langgraph.json 구성에서 UI 컴포넌트를 정의합니다:
{
  "node_version": "20",
  "graphs": {
    "agent": "./src/agent/index.ts:graph"
  },
  "ui": {
    "agent": "./src/agent/ui.tsx"
  }
}
ui 섹션은 graph에서 사용할 UI 컴포넌트를 가리킵니다. 기본적으로 graph 이름과 동일한 키를 사용하는 것을 권장하지만, 원하는 대로 컴포넌트를 분할할 수 있습니다. 자세한 내용은 UI 컴포넌트의 namespace 사용자 정의를 참조하세요. LangSmith는 UI 컴포넌트 코드와 스타일을 자동으로 번들링하고 LoadExternalComponent 컴포넌트에서 로드할 수 있는 외부 자산으로 제공합니다. reactreact-dom과 같은 일부 종속성은 번들에서 자동으로 제외됩니다. CSS와 Tailwind 4.x도 기본적으로 지원되므로 UI 컴포넌트에서 Tailwind 클래스와 shadcn/ui를 자유롭게 사용할 수 있습니다.
  • src/agent/ui.tsx
  • src/agent/styles.css
import "./styles.css";

const WeatherComponent = (props: { city: string }) => {
  return <div className="bg-red-500">Weather for {props.city}</div>;
};

export default {
  weather: WeatherComponent,
};

2. graph에서 UI 컴포넌트 전송

  • Python
  • JS
src/agent.py
import uuid
from typing import Annotated, Sequence, TypedDict

from langchain.messages import AIMessage, BaseMessage
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph
from langgraph.graph.message import add_messages
from langgraph.graph.ui import AnyUIMessage, ui_message_reducer, push_ui_message


class AgentState(TypedDict):  # noqa: D101
    messages: Annotated[Sequence[BaseMessage], add_messages]
    ui: Annotated[Sequence[AnyUIMessage], ui_message_reducer]


async def weather(state: AgentState):
    class WeatherOutput(TypedDict):
        city: str

    weather: WeatherOutput = (
        await ChatOpenAI(model="gpt-4o-mini")
        .with_structured_output(WeatherOutput)
        .with_config({"tags": ["nostream"]})
        .ainvoke(state["messages"])
    )

    message = AIMessage(
        id=str(uuid.uuid4()),
        content=f"Here's the weather for {weather['city']}",
    )

    # Emit UI elements associated with the message
    push_ui_message("weather", weather, message=message)
    return {"messages": [message]}


workflow = StateGraph(AgentState)
workflow.add_node(weather)
workflow.add_edge("__start__", "weather")
graph = workflow.compile()

3. React 애플리케이션에서 UI 요소 처리

클라이언트 측에서는 useStream()LoadExternalComponent를 사용하여 UI 요소를 표시할 수 있습니다.
src/app/page.tsx
"use client";

import { useStream } from "@langchain/langgraph-sdk/react";
import { LoadExternalComponent } from "@langchain/langgraph-sdk/react-ui";

export default function Page() {
  const { thread, values } = useStream({
    apiUrl: "http://localhost:2024",
    assistantId: "agent",
  });

  return (
    <div>
      {thread.messages.map((message) => (
        <div key={message.id}>
          {message.content}
          {values.ui
            ?.filter((ui) => ui.metadata?.message_id === message.id)
            .map((ui) => (
              <LoadExternalComponent key={ui.id} stream={thread} message={ui} />
            ))}
        </div>
      ))}
    </div>
  );
}
내부적으로 LoadExternalComponent는 LangSmith에서 UI 컴포넌트의 JS와 CSS를 가져와 shadow DOM에서 렌더링하여 애플리케이션의 나머지 부분과 스타일 격리를 보장합니다.

How-to 가이드

클라이언트 측에서 커스텀 컴포넌트 제공

클라이언트 애플리케이션에 이미 로드된 컴포넌트가 있는 경우, LangSmith에서 UI 코드를 가져오지 않고 직접 렌더링할 컴포넌트 맵을 제공할 수 있습니다.
const clientComponents = {
  weather: WeatherComponent,
};

<LoadExternalComponent
  stream={thread}
  message={ui}
  components={clientComponents}
/>;

컴포넌트 로딩 중 로딩 UI 표시

컴포넌트가 로딩 중일 때 렌더링할 fallback UI를 제공할 수 있습니다.
<LoadExternalComponent
  stream={thread}
  message={ui}
  fallback={<div>Loading...</div>}
/>

UI 컴포넌트의 namespace 사용자 정의

기본적으로 LoadExternalComponentuseStream() hook의 assistantId를 사용하여 UI 컴포넌트의 코드를 가져옵니다. LoadExternalComponent 컴포넌트에 namespace prop을 제공하여 이를 사용자 정의할 수 있습니다.
  • src/app/page.tsx
  • langgraph.json
<LoadExternalComponent
  stream={thread}
  message={ui}
  namespace="custom-namespace"
/>

UI 컴포넌트에서 thread state 접근 및 상호작용

useStreamContext hook을 사용하여 UI 컴포넌트 내부에서 thread state에 접근할 수 있습니다.
import { useStreamContext } from "@langchain/langgraph-sdk/react-ui";

const WeatherComponent = (props: { city: string }) => {
  const { thread, submit } = useStreamContext();
  return (
    <>
      <div>Weather for {props.city}</div>

      <button
        onClick={() => {
          const newMessage = {
            type: "human",
            content: `What's the weather in ${props.city}?`,
          };

          submit({ messages: [newMessage] });
        }}
      >
        Retry
      </button>
    </>
  );
};

클라이언트 컴포넌트에 추가 컨텍스트 전달

LoadExternalComponent 컴포넌트에 meta prop을 제공하여 클라이언트 컴포넌트에 추가 컨텍스트를 전달할 수 있습니다.
<LoadExternalComponent stream={thread} message={ui} meta={{ userId: "123" }} />
그런 다음 useStreamContext hook을 사용하여 UI 컴포넌트에서 meta prop에 접근할 수 있습니다.
import { useStreamContext } from "@langchain/langgraph-sdk/react-ui";

const WeatherComponent = (props: { city: string }) => {
  const { meta } = useStreamContext<
    { city: string },
    { MetaType: { userId?: string } }
  >();

  return (
    <div>
      Weather for {props.city} (user: {meta?.userId})
    </div>
  );
};

서버에서 UI 메시지 스트리밍

useStream() hook의 onCustomEvent 콜백을 사용하여 노드 실행이 완료되기 전에 UI 메시지를 스트리밍할 수 있습니다. 이는 LLM이 응답을 생성하는 동안 UI 컴포넌트를 업데이트할 때 특히 유용합니다.
import { uiMessageReducer } from "@langchain/langgraph-sdk/react-ui";

const { thread, submit } = useStream({
  apiUrl: "http://localhost:2024",
  assistantId: "agent",
  onCustomEvent: (event, options) => {
    options.mutate((prev) => {
      const ui = uiMessageReducer(prev.ui ?? [], event);
      return { ...prev, ui };
    });
  },
});
그런 다음 업데이트하려는 UI 메시지와 동일한 ID로 ui.push() / push_ui_message()를 호출하여 UI 컴포넌트에 업데이트를 푸시할 수 있습니다.
  • Python
  • JS
  • ui.tsx
from typing import Annotated, Sequence, TypedDict

from langchain_anthropic import ChatAnthropic
from langchain.messages import AIMessage, AIMessageChunk, BaseMessage
from langgraph.graph import StateGraph
from langgraph.graph.message import add_messages
from langgraph.graph.ui import AnyUIMessage, push_ui_message, ui_message_reducer


class AgentState(TypedDict):  # noqa: D101
    messages: Annotated[Sequence[BaseMessage], add_messages]
    ui: Annotated[Sequence[AnyUIMessage], ui_message_reducer]


class CreateTextDocument(TypedDict):
    """Prepare a document heading for the user."""

    title: str


async def writer_node(state: AgentState):
    model = ChatAnthropic(model="claude-3-5-sonnet-latest")
    message: AIMessage = await model.bind_tools(
        tools=[CreateTextDocument],
        tool_choice={"type": "tool", "name": "CreateTextDocument"},
    ).ainvoke(state["messages"])

    tool_call = next(
        (x["args"] for x in message.tool_calls if x["name"] == "CreateTextDocument"),
        None,
    )

    if tool_call:
        ui_message = push_ui_message("writer", tool_call, message=message)
        ui_message_id = ui_message["id"]

        # We're already streaming the LLM response to the client through UI messages
        # so we don't need to stream it again to the `messages` stream mode.
        content_stream = model.with_config({"tags": ["nostream"]}).astream(
            f"Create a document with the title: {tool_call['title']}"
        )

        content: AIMessageChunk | None = None
        async for chunk in content_stream:
            content = content + chunk if content else chunk

            push_ui_message(
                "writer",
                {"content": content.text()},
                id=ui_message_id,
                message=message,
                # Use `merge=rue` to merge props with the existing UI message
                merge=True,
            )

    return {"messages": [message]}

state에서 UI 메시지 제거

RemoveMessage를 추가하여 state에서 메시지를 제거할 수 있는 것과 유사하게, UI 메시지의 ID로 remove_ui_message / ui.delete를 호출하여 state에서 UI 메시지를 제거할 수 있습니다.
  • Python
  • JS
from langgraph.graph.ui import push_ui_message, delete_ui_message

# push message
message = push_ui_message("weather", {"city": "London"})

# remove said message
delete_ui_message(message["id"])

더 알아보기


Connect these docs programmatically to Claude, VSCode, and more via MCP for real-time answers.
I