From 2d97af57f8c1399a940490e05d29fcad8b0cf6c7 Mon Sep 17 00:00:00 2001 From: Tat Dat Duong Date: Thu, 13 Feb 2025 07:08:35 -0800 Subject: [PATCH] Add basic docs --- docs/docs/how-tos/use-stream-react.md | 432 ++++++++++++++++++++++++++ 1 file changed, 432 insertions(+) create mode 100644 docs/docs/how-tos/use-stream-react.md diff --git a/docs/docs/how-tos/use-stream-react.md b/docs/docs/how-tos/use-stream-react.md new file mode 100644 index 000000000..45cf5450d --- /dev/null +++ b/docs/docs/how-tos/use-stream-react.md @@ -0,0 +1,432 @@ +# How to stream runs into an React app + +!!! info "Prerequisites" - [LangGraph Platform](../concepts/langgraph_platform.md) - [LangGraph Server](../concepts/langgraph_server.md) + +The `useStream()` hook allows you to easily stream values from a LangGraph run. It enables the following features: + +- Streaming messages: Streams messages from the run as they are generated. +- State management: Thread state is managed for you, including messages, loading and error states. +- Branching support: We handle checkpoint branching for you, so you can focus on building your chat interface. +- Headless: Bring your own chat UI and implement streaming into any design or layout. + +This guide will show you how you can use `useStream()` to stream values within your React application. + +## Example + +```tsx +"use client"; + +import { useStream } from "@langchain/langgraph-sdk/react"; +import type { Message } from "@langchain/langgraph-sdk"; + +export default function App() { + const thread = useStream<{ messages: Message[] }>({ + apiUrl: "http://localhost:2024", + assistantId: "agent", + messagesKey: "messages", + }); + + return ( +
+
+ {thread.messages.map((message) => ( +
{message.content as string}
+ ))} +
+ +
{ + e.preventDefault(); + + const form = e.target as HTMLFormElement; + const message = new FormData(form).get("message") as string; + + form.reset(); + thread.submit({ messages: [{ type: "human", content: message }] }); + }} + > + + + {thread.isLoading ? ( + + ) : ( + + )} +
+
+ ); +} +``` + +## Customise UI + +The `useStream()` hook provides built-in state management capabilities to simplify your application development. It handles: + +- Thread state management +- Loading states during stream operations +- Error handling and error states +- Message management + +This allows you to focus on building your UI while the `useStream()` hook takes care of the underlying state complexity. + +### Loading state + +The `isLoading` property is set to `true` whenever the stream is running. This is useful for: + +1. Showing a loading spinner to indicate that the stream is running. +2. Disabling the input box to prevent multiple submissions. +3. Showing a cancellation button to cancel a run. + +```tsx +export default function App() { + const { isLoading, stop } = useStream<{ messages: Message[] }>({ + apiUrl: "http://localhost:2024", + assistantId: "agent", + messagesKey: "messages", + }); + + return ( +
+ {isLoading && ( + + )} +
+ ); +} +``` + +### Thread management + +The `useStream()` hook manages a thread for you. You can use the `threadId` property to get the thread ID. Pass in the `onThreadId` callback to get notified when the new thread is created. + +```tsx +const [threadId, setThreadId] = useState(null); + +const thread = useStream<{ messages: Message[] }>({ + apiUrl: "http://localhost:2024", + assistantId: "agent", + + threadId: threadId, + onThreadId: setThreadId, +}); +``` + +We recommend setting the `threadId` as a query parameter in the URL, so that you can resume the conversation from the same thread even when the page is refreshed. + +### Messages handling + +To enable messages handling, you need to pass the `messagesKey` option to the `useStream()` hook. When enabled, the `useStream()` hook will keep track of the message chunks received from the server and concatenate them together to form a complete message. The completed message chunks can be retrieved via the `messages` property. + +```tsx +import type { Message } from "@langchain/langgraph-sdk"; +import { useStream } from "@langchain/langgraph-sdk/react"; + +export default function HomePage() { + const thread = useStream<{ messages: Message[] }>({ + apiUrl: "http://localhost:2024", + assistantId: "agent", + messagesKey: "messages", + }); + + return ( +
+ {thread.messages.map((message) => ( +
{message.content as string}
+ ))} +
+ ); +} +``` + +### Branching + +To enable branching, you need to enable messages handling. Pass the `messagesKey` option to the `useStream()` hook. For each message, you can use `getMessagesMetadata()` to get the first checkpoint from which the message has been first seen. You can then create a new run from the checkpoint preceding the first seen checkpoint to create a new branch in a thread. + +A branch can be created in following ways: + +1. Edit a previous user message. +2. Request a regeneration of a previous assistant message. + +```tsx +/* eslint-disable @typescript-eslint/no-floating-promises */ +"use client"; + +import type { Message } from "@langchain/langgraph-sdk"; +import { useStream } from "@langchain/langgraph-sdk/react"; +import { + Annotation, + MessagesAnnotation, + type StateType, + type UpdateType, +} from "@langchain/langgraph/web"; +import { useState } from "react"; + +const AgentState = Annotation.Root({ + ...MessagesAnnotation.spec, +}); + +function BranchSwitcher({ + branch, + branchOptions, + onSelect, +}: { + branch: string | undefined; + branchOptions: string[] | undefined; + onSelect: (branch: string) => void; +}) { + if (!branchOptions || !branch) return null; + const index = branchOptions.indexOf(branch); + + return ( +
+ + + {index + 1} / {branchOptions.length} + + +
+ ); +} + +function EditMessage({ + message, + onEdit, +}: { + message: Message; + onEdit: (message: Message) => void; +}) { + const [editing, setEditing] = useState(false); + + if (!editing) { + return ( + + ); + } + + return ( +
{ + e.preventDefault(); + const form = e.target as HTMLFormElement; + const content = new FormData(form).get("content") as string; + + form.reset(); + onEdit({ type: "human", content }); + setEditing(false); + }} + > + + +
+ ); +} + +export default function App() { + const thread = useStream< + StateType, + UpdateType + >({ + apiUrl: "http://localhost:2024", + assistantId: "agent", + messagesKey: "messages", + }); + + return ( +
+
+ {thread.messages.map((message) => { + const meta = thread.getMessagesMetadata(message); + const parentCheckpoint = meta?.firstSeenState?.parent_checkpoint; + + return ( +
+
{message.content as string}
+ + {message.type === "human" && ( + + thread.submit( + { messages: [message] }, + { checkpoint: parentCheckpoint }, + ) + } + /> + )} + + {message.type === "ai" && ( + + )} + + thread.setBranch(branch)} + /> +
+ ); + })} +
+ +
{ + e.preventDefault(); + + const form = e.target as HTMLFormElement; + const message = new FormData(form).get("message") as string; + + form.reset(); + thread.submit({ messages: [message] }); + }} + > + + + {thread.isLoading ? ( + + ) : ( + + )} +
+
+ ); +} +``` + +### TypeScript and Type safety + +The `useStream()` hook accepts generic parameters that can be used to specify the thread state and update type as well as the custom event type, avoiding the need to manually type-cast. + +```tsx +// Type definition of the state +type StateType = { messages: Message[] }; + +// Type definition of the update +type UpdateType = { messages: Message[] | Message }; + +// Type definition of the custom event +type CustomEventType = { counter: number }; + +const thread = useStream({ + apiUrl: "http://localhost:2024", + assistantId: "agent", + messagesKey: "messages", +}); +``` + +If you use `LangGraph.js`, you can re-use the same `Annotation` as the one used within `StateGraph`. + +!!! warning "Importing from @langchain/langgraph/web" + + Make sure to import from `@langchain/langgraph/web` and not from `@langchain/langgraph`, as the default entrypoint will attempt to initialize `AsyncLocalStorage`, which is not available in the browser. + +```tsx +"use client"; + +import { useStream } from "@langchain/langgraph-sdk/react"; +import { + Annotation, + MessagesAnnotation, + type StateType, + type UpdateType, +} from "@langchain/langgraph/web"; + +const AgentState = Annotation.Root({ + ...MessagesAnnotation.spec, +}); + +export default function HomePage() { + const thread = useStream< + StateType, + UpdateType + >({ + apiUrl: "http://localhost:2024", + assistantId: "agent", + messagesKey: "messages", + }); + + return ( +
+
+ {thread.messages.map((message) => ( +
{message.content as string}
+ ))} +
+ +
{ + e.preventDefault(); + + const form = e.target as HTMLFormElement; + const message = new FormData(form).get("message") as string; + + form.reset(); + thread.submit({ messages: [message] }); + }} + > + + + {thread.isLoading ? ( + + ) : ( + + )} +
+
+ ); +} +``` + +## Event callbacks + +The `useStream()` hook provides few event callbacks that you can use to react to specific events. + +- `onError`: Called when an error occurs. +- `onFinish`: Called when the stream is finished. +- `onUpdateEvent`: Called when an update event is received. +- `onCustomEvent`: Called when a custom event is received. See [Custom events](../concepts/custom-events.md) to learn how to stream custom events. +- `onMetadataEvent`: Called when a metadata event is received. + +## Learn more + +TODO: add a link to the `useStream()` hook documentation.