assistant-ui
assistant-ui (assistant-ui)
assistant-ui는 AI 채팅을 위한 헤드리스(headless) React UI 프레임워크예요. 스레드 관리, 메시지 분기, 첨부 파일 처리 같은 완전한 런타임 레이어를 제공하며, useExternalStoreRuntime 어댑터를 통해 LangChain의 useStream과 연결됩니다. 즉 assistant-ui의 강력한 런타임 기능을 그대로 둔 채, 백엔드와의 통신은 LangChain 에이전트가 담당하는 구조죠.
참고: 전체 assistant-ui 예제를 langgraphjs 예제 저장소에서 클론해서 실행해보면,
useExternalStoreRuntime으로 LangChain 에이전트에 연결된 Claude 스타일 채팅 인터페이스를 직접 볼 수 있어요.
동작 방식 (How it works)
useExternalStoreRuntime으로 어댑트 —BaseMessage[]를ThreadMessageLike[]로 변환해stream.messages를 assistant-ui의 런타임 형식에 브리징해요.- 런타임 제공 — UI를
AssistantRuntimeProvider로 감싸고 아무 assistant-ui 스레드 컴포넌트를 렌더링합니다.
useStream 연결하기 (Wiring useStream)
useExternalStoreRuntime이 assistant-ui와 LangChain 스트림을 이어주는 핵심이에요. 입력 제출과 메시지 변환을 모두 여기서 처리합니다.
const text = message.content
.filter((c) => c.type === "text")
.map((c) => c.text)
.join("");
await stream.submit({ messages: [{ type: "human", content: text }] });
},
[stream],
);
// Convert LangChain messages to assistant-ui's ThreadMessageLike format
const messages = useMemo(
() => toThreadMessages(stream.messages),
[stream.messages],
);
const runtime = useExternalStoreRuntime<ThreadMessageLike>({
// ...
});
메시지 변환 (Converting messages)
toThreadMessages는 LangChain BaseMessage[]를 assistant-ui가 기대하는 ThreadMessageLike[] 형식으로 매핑해요. human·AI·tool 각 메시지 타입을 처리하고, 콘텐츠 블록, 도구 호출, 추론 토큰도 변환합니다.
import { AIMessage, HumanMessage, ToolMessage, type BaseMessage } from "langchain";
import type { ThreadMessageLike } from "@assistant-ui/react";
export function toThreadMessages(messages: BaseMessage[]): ThreadMessageLike[] {
const result: ThreadMessageLike[] = [];
for (const msg of messages) {
if (HumanMessage.isInstance(msg)) {
result.push({
role: "user",
content: [{ type: "text", text: msg.text }],
});
} else if (AIMessage.isInstance(msg)) {
const parts: ThreadMessageLike["content"] = [];
// Reasoning tokens
const reasoning = msg.contentBlocks.find((block) => block.type === "reasoning")?.reasoning;
if (reasoning) parts.push({ type: "reasoning", text: reasoning });
// Tool calls
for (const tc of msg.tool_calls ?? []) {
parts.push({
type: "tool-call",
toolCallId: tc.id ?? "",
// ...
});
}
// ...
}
}
return result;
}
모범 사례 (Best practices)
- 스레드 영속성 —
onThreadId로threadId를 영속화하고, 페이지 로드 시useStream에 다시 넘겨서 assistant-ui가 같은 스레드에 다시 연결되게 하세요. - 변환은 한 번에 —
useMemo로 메시지 변환 결과를 기억해 불필요한 재변환을 피하세요. - 타입 분기는
isInstance로 —getType()보다HumanMessage.isInstance/AIMessage.isInstance를 써서 TypeScript 내로잉을 제대로 활용하세요.