From 2d97af57f8c1399a940490e05d29fcad8b0cf6c7 Mon Sep 17 00:00:00 2001 From: Tat Dat Duong Date: Thu, 13 Feb 2025 07:08:35 -0800 Subject: [PATCH 01/11] 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. From 49c74dd569ff70ebcc4866327dc59191ac693664 Mon Sep 17 00:00:00 2001 From: Tat Dat Duong Date: Thu, 13 Feb 2025 07:12:15 -0800 Subject: [PATCH 02/11] Add docs --- libs/sdk-js/src/react/stream.tsx | 93 +++++++++++++++++++++++--------- 1 file changed, 67 insertions(+), 26 deletions(-) diff --git a/libs/sdk-js/src/react/stream.tsx b/libs/sdk-js/src/react/stream.tsx index 0f229da40..88190be46 100644 --- a/libs/sdk-js/src/react/stream.tsx +++ b/libs/sdk-js/src/react/stream.tsx @@ -179,26 +179,79 @@ function useThreadHistory>( }; } +const useControllableThreadId = (options?: { + threadId?: string | null; + onThreadId?: (threadId: string) => void; +}): [string | null, (threadId: string) => void] => { + const [localThreadId, _setLocalThreadId] = useState( + options?.threadId ?? null, + ); + + const onThreadIdRef = useRef(options?.onThreadId); + onThreadIdRef.current = options?.onThreadId; + + const onThreadId = useCallback((threadId: string) => { + _setLocalThreadId(threadId); + onThreadIdRef.current?.(threadId); + }, []); + + if (typeof options?.threadId === "undefined") { + return [localThreadId, onThreadId]; + } + + return [options.threadId, onThreadId]; +}; + export function useStream< StateType extends Record = Record, UpdateType extends Record = Partial, CustomType = unknown, >(options: { + /** + * The ID of the assistant to use. + */ assistantId: string; + /** + * The URL of the API to use. + */ apiUrl: ClientConfig["apiUrl"]; + + /** + * The API key to use. + */ apiKey?: ClientConfig["apiKey"]; - withMessages?: string; + /** + * Specify the key within the state that contains messages. + */ + messagesKey?: string; + /** + * Callback that is called when an error occurs. + */ onError?: (error: unknown) => void; + + /** + * Callback that is called when the stream is finished. + */ onFinish?: (state: ThreadState) => void; + /** + * Callback that is called when an update event is received. + */ onUpdateEvent?: (data: UpdatesStreamEvent["data"]) => void; + + /** + * Callback that is called when a custom event is received. + */ onCustomEvent?: (data: CustomStreamEvent["data"]) => void; + + /** + * Callback that is called when a metadata event is received. + */ onMetadataEvent?: (data: MetadataStreamEvent["data"]) => void; - // TODO: can we make threadId uncontrollable / controllable? threadId?: string | null; onThreadId?: (threadId: string) => void; }) { @@ -214,11 +267,12 @@ export function useStream< | ErrorStreamEvent | FeedbackStreamEvent; - const { assistantId, threadId, withMessages, onError, onFinish } = options; + const { assistantId, messagesKey, onError, onFinish } = options; const client = useMemo( () => new Client({ apiUrl: options.apiUrl, apiKey: options.apiKey }), [options.apiKey, options.apiUrl], ); + const [threadId, onThreadId] = useControllableThreadId(options); const [branchPath, setBranchPath] = useState([]); const [isLoading, setIsLoading] = useState(false); @@ -269,12 +323,12 @@ export function useStream< ); const getMessages = useMemo(() => { - if (withMessages == null) return undefined; + if (messagesKey == null) return undefined; return (value: StateType) => - Array.isArray(value[withMessages]) - ? (value[withMessages] as Message[]) + Array.isArray(value[messagesKey]) + ? (value[messagesKey] as Message[]) : []; - }, [withMessages]); + }, [messagesKey]); const [sequence, pathMap] = (() => { const childrenMap: Record[]> = {}; @@ -472,7 +526,7 @@ export function useStream< let usableThreadId = threadId; if (!usableThreadId) { const thread = await client.threads.create(); - options?.onThreadId?.(thread.thread_id); + onThreadId(thread.thread_id); usableThreadId = thread.thread_id; } @@ -537,22 +591,11 @@ export function useStream< break; } - if (event === "updates") { - options.onUpdateEvent?.(data); - } - - if (event === "custom") { - options.onCustomEvent?.(data); - } - - if (event === "metadata") { - options.onMetadataEvent?.(data); - } - - if (event === "values") { - setStreamValues(data); - } + if (event === "updates") options.onUpdateEvent?.(data); + if (event === "custom") options.onCustomEvent?.(data); + if (event === "metadata") options.onMetadataEvent?.(data); + if (event === "values") setStreamValues(data); if (event === "messages") { if (!getMessages) continue; @@ -577,15 +620,13 @@ export function useStream< if (!chunk || index == null) return values; messages[index] = toMessageDict(chunk); - return { ...values, [withMessages!]: messages }; + return { ...values, [messagesKey!]: messages }; }); } } // TODO: stream created checkpoints to avoid an unnecessary network request const result = await history.mutate(usableThreadId); - - // TODO: write tests verifying that stream values are properly handled lifecycle-wise setStreamValues(null); if (streamError != null) throw streamError; From 208d9d165d64162ab4e9af9d7bc600462ebbdd76 Mon Sep 17 00:00:00 2001 From: Tat Dat Duong Date: Thu, 13 Feb 2025 07:44:01 -0800 Subject: [PATCH 03/11] feat(sdk-js): add docs, how-to guide --- .../how-tos/use_stream_react.md} | 158 ++++++++---------- libs/sdk-js/.gitignore | 1 + libs/sdk-js/package.json | 9 +- libs/sdk-js/typedoc.react.json | 5 + libs/sdk-js/yarn.lock | 84 +++++++--- 5 files changed, 135 insertions(+), 122 deletions(-) rename docs/docs/{how-tos/use-stream-react.md => cloud/how-tos/use_stream_react.md} (63%) create mode 100644 libs/sdk-js/typedoc.react.json diff --git a/docs/docs/how-tos/use-stream-react.md b/docs/docs/cloud/how-tos/use_stream_react.md similarity index 63% rename from docs/docs/how-tos/use-stream-react.md rename to docs/docs/cloud/how-tos/use_stream_react.md index 45cf5450d..66a7fb9ed 100644 --- a/docs/docs/how-tos/use-stream-react.md +++ b/docs/docs/cloud/how-tos/use_stream_react.md @@ -1,15 +1,19 @@ -# How to stream runs into an React app +# How to Stream LangGraph Runs in Your React App -!!! info "Prerequisites" - [LangGraph Platform](../concepts/langgraph_platform.md) - [LangGraph Server](../concepts/langgraph_server.md) +!!! 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: +The `useStream()` React hook provides a seamless way to integrate LangGraph runs into your React applications. It handles all the complexities of streaming, state management, and branching logic, letting you focus on building great chat experiences. -- 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. +Key features: -This guide will show you how you can use `useStream()` to stream values within your React application. +- Messages streaming: Handle a stream of message chunks to form a complete message +- Automatic state management for messages, loading states, and errors +- Conversation branching: Create alternate conversation paths from any point in the chat history +- UI-agnostic design - bring your own components and styling + +Let's explore how to use `useStream()` in your React application. ## Example @@ -62,24 +66,24 @@ export default function App() { } ``` -## Customise UI +## Customizing Your UI -The `useStream()` hook provides built-in state management capabilities to simplify your application development. It handles: +The `useStream()` hook takes care of all the complex state management behind the scenes, providing you with simple interfaces to build your UI. Here's what you get out of the box: - Thread state management -- Loading states during stream operations -- Error handling and error states -- Message management +- Loading and error states +- Message handling and updates +- Branching support -This allows you to focus on building your UI while the `useStream()` hook takes care of the underlying state complexity. +Here are some examples on how to use these features effectively: -### Loading state +### Loading States -The `isLoading` property is set to `true` whenever the stream is running. This is useful for: +The `isLoading` property tells you when a stream is active, enabling you to: -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. +- Show a loading indicator +- Disable input fields during processing +- Display a cancel button ```tsx export default function App() { @@ -101,9 +105,9 @@ export default function App() { } ``` -### Thread management +### 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. +Keep track of conversations with built-in thread management. You can access the current thread ID and get notified when new threads are created: ```tsx const [threadId, setThreadId] = useState(null); @@ -117,11 +121,13 @@ const thread = useStream<{ messages: Message[] }>({ }); ``` -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. +We recommend storing the `threadId` in your URL's query parameters to let users resume conversations after page refreshes. -### Messages handling +### 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. +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"; @@ -144,7 +150,7 @@ export default function HomePage() { } ``` -### Branching +### Branching Support 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. @@ -274,7 +280,7 @@ export default function App() { onEdit={(message) => thread.submit( { messages: [message] }, - { checkpoint: parentCheckpoint }, + { checkpoint: parentCheckpoint } ) } /> @@ -329,37 +335,42 @@ export default function App() { } ``` -### TypeScript and Type safety +### TypeScript -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. +The `useStream()` hook is fully typed to help catch errors early and provide better IDE support. You can specify types for: + +- State shape +- Update format +- Custom events ```tsx -// Type definition of the state -type StateType = { messages: Message[] }; +// Define your types +type State = { + messages: Message[]; + context?: Record; +}; -// Type definition of the update -type UpdateType = { messages: Message[] | Message }; +type Update = { + messages: Message[] | Message; + context?: Record; +}; -// Type definition of the custom event -type CustomEventType = { counter: number }; +type CustomEvent = { + type: "progress" | "debug"; + payload: unknown; +}; -const thread = useStream({ +// Use them with the hook +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. +If you're using LangGraph.js, you can reuse your graph's annotation types: ```tsx -"use client"; - -import { useStream } from "@langchain/langgraph-sdk/react"; import { Annotation, MessagesAnnotation, @@ -369,57 +380,22 @@ import { const AgentState = Annotation.Root({ ...MessagesAnnotation.spec, + context: Annotation.Optional(Annotation.Any()), }); -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 ? ( - - ) : ( - - )} -
-
- ); -} +const thread = useStream< + StateType, + UpdateType +>({ + apiUrl: "http://localhost:2024", + assistantId: "agent", + messagesKey: "messages", +}); ``` -## Event callbacks +## Event Handling -The `useStream()` hook provides few event callbacks that you can use to react to specific events. +The `useStream()` hook provides several callback options to help you respond to different events: - `onError`: Called when an error occurs. - `onFinish`: Called when the stream is finished. @@ -427,6 +403,6 @@ The `useStream()` hook provides few event callbacks that you can use to react to - `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 +## Learn More -TODO: add a link to the `useStream()` hook documentation. +- [JS/TS SDK Reference](../reference/sdk/js_ts_sdk_ref.md) diff --git a/libs/sdk-js/.gitignore b/libs/sdk-js/.gitignore index 4d7124e5f..8eaba233a 100644 --- a/libs/sdk-js/.gitignore +++ b/libs/sdk-js/.gitignore @@ -13,3 +13,4 @@ react.d.cts node_modules dist .yarn +docs \ No newline at end of file diff --git a/libs/sdk-js/package.json b/libs/sdk-js/package.json index 759708e4d..cb34bb373 100644 --- a/libs/sdk-js/package.json +++ b/libs/sdk-js/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/langgraph-sdk", - "version": "0.0.40", + "version": "0.0.41", "description": "Client library for interacting with the LangGraph API", "type": "module", "packageManager": "yarn@1.22.19", @@ -10,7 +10,8 @@ "prepublish": "yarn run build", "format": "prettier --write src", "lint": "prettier --check src && tsc --noEmit", - "test": "NODE_OPTIONS=--experimental-vm-modules jest --testPathIgnorePatterns=\\.int\\.test.ts" + "test": "NODE_OPTIONS=--experimental-vm-modules jest --testPathIgnorePatterns=\\.int\\.test.ts", + "typedoc": "typedoc && typedoc src/react/index.ts --out docs/react --options typedoc.react.json" }, "main": "index.js", "license": "MIT", @@ -33,8 +34,8 @@ "jest": "^29.7.0", "prettier": "^3.2.5", "ts-jest": "^29.1.2", - "typedoc": "^0.26.1", - "typedoc-plugin-markdown": "^4.1.0", + "typedoc": "^0.27.7", + "typedoc-plugin-markdown": "^4.4.2", "typescript": "^5.4.5", "react": "^18.3.1" }, diff --git a/libs/sdk-js/typedoc.react.json b/libs/sdk-js/typedoc.react.json new file mode 100644 index 000000000..6359dac1b --- /dev/null +++ b/libs/sdk-js/typedoc.react.json @@ -0,0 +1,5 @@ +{ + "pageTitleTemplates": { + "index": "{projectName}/react" + } +} diff --git a/libs/sdk-js/yarn.lock b/libs/sdk-js/yarn.lock index 4d4e4ccf8..635f0c5f3 100644 --- a/libs/sdk-js/yarn.lock +++ b/libs/sdk-js/yarn.lock @@ -301,6 +301,15 @@ resolved "https://registry.yarnpkg.com/@cfworker/json-schema/-/json-schema-4.1.1.tgz#4a2a3947ee9fa7b7c24be981422831b8674c3be6" integrity sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og== +"@gerrit0/mini-shiki@^1.24.0": + version "1.27.2" + resolved "https://registry.yarnpkg.com/@gerrit0/mini-shiki/-/mini-shiki-1.27.2.tgz#cf2a9fcb08a6581c78fc94821f0c854ec4b9f899" + integrity sha512-GeWyHz8ao2gBiUW4OJnQDxXQnFgZQwwQk05t/CVVgNBN7/rK8XZ7xY6YhLVv9tH3VppWWmr9DCl3MwemB/i+Og== + dependencies: + "@shikijs/engine-oniguruma" "^1.27.2" + "@shikijs/types" "^1.27.2" + "@shikijs/vscode-textmate" "^10.0.1" + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" @@ -806,10 +815,26 @@ optionalDependencies: fsevents "~2.3.2" -"@shikijs/core@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@shikijs/core/-/core-1.9.0.tgz#ff717fef5e0e9882f0848272699fd8f04d6f9a07" - integrity sha512-cbSoY8P/jgGByG8UOl3jnP/CWg/Qk+1q+eAKWtcrU3pNoILF8wTsLB0jT44qUBV8Ce1SvA9uqcM9Xf+u3fJFBw== +"@shikijs/engine-oniguruma@^1.27.2": + version "1.29.2" + resolved "https://registry.yarnpkg.com/@shikijs/engine-oniguruma/-/engine-oniguruma-1.29.2.tgz#d879717ced61d44e78feab16f701f6edd75434f1" + integrity sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA== + dependencies: + "@shikijs/types" "1.29.2" + "@shikijs/vscode-textmate" "^10.0.1" + +"@shikijs/types@1.29.2", "@shikijs/types@^1.27.2": + version "1.29.2" + resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-1.29.2.tgz#a93fdb410d1af8360c67bf5fc1d1a68d58e21c4f" + integrity sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw== + dependencies: + "@shikijs/vscode-textmate" "^10.0.1" + "@types/hast" "^3.0.4" + +"@shikijs/vscode-textmate@^10.0.1": + version "10.0.1" + resolved "https://registry.yarnpkg.com/@shikijs/vscode-textmate/-/vscode-textmate-10.0.1.tgz#d06d45b67ac5e9b0088e3f67ebd3f25c6c3d711a" + integrity sha512-fTIQwLF+Qhuws31iw7Ncl1R3HUDtGwIipiJ9iU+UsDUwMhegFcQKQHd51nZjb7CArq0MvON8rbgCGQYWHUKAdg== "@sinclair/typebox@^0.27.8": version "0.27.8" @@ -910,6 +935,13 @@ dependencies: "@types/node" "*" +"@types/hast@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa" + integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ== + dependencies: + "@types/unist" "*" + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.6" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" @@ -996,6 +1028,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== +"@types/unist@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c" + integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q== + "@types/unist@^2", "@types/unist@^2.0.0", "@types/unist@^2.0.2": version "2.0.10" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.10.tgz#04ffa7f406ab628f7f7e97ca23e290cd8ab15efc" @@ -3271,7 +3308,7 @@ minimatch@^5.0.1: dependencies: brace-expansion "^2.0.1" -minimatch@^9.0.3: +minimatch@^9.0.3, minimatch@^9.0.5: version "9.0.5" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== @@ -3857,13 +3894,6 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shiki@^1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/shiki/-/shiki-1.9.0.tgz#e4d3a044d9c746aefbea47615e83323fdc3dc361" - integrity sha512-i6//Lqgn7+7nZA0qVjoYH0085YdNk4MC+tJV4bo+HgjgRMJ0JmkLZzFAuvVioJqLkcGDK5GAMpghZEZkCnwxpQ== - dependencies: - "@shikijs/core" "1.9.0" - side-channel@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" @@ -4231,21 +4261,21 @@ typedarray.prototype.slice@^1.0.3: typed-array-buffer "^1.0.2" typed-array-byte-offset "^1.0.2" -typedoc-plugin-markdown@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.1.0.tgz#0969e82d9821c956145a4b8a9a70f4e00bde27e8" - integrity sha512-sUiEJVaa6+MOFShRy14j1OP/VXC5OLyHNecJ2nKeGuBy2M3YiMatSLoIiddFAqVptSuILJTZiJzCBIY6yzAVyg== +typedoc-plugin-markdown@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.4.2.tgz#fc31779595aa9bf00e66709f3894e048345bf7ed" + integrity sha512-kJVkU2Wd+AXQpyL6DlYXXRrfNrHrEIUgiABWH8Z+2Lz5Sq6an4dQ/hfvP75bbokjNDUskOdFlEEm/0fSVyC7eg== -typedoc@^0.26.1: - version "0.26.1" - resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.26.1.tgz#fc43108abdea64929a2e636877e250d5dea50957" - integrity sha512-APsVXqh93jTlpkLuw6+/IORx7n5LN8hzJV8nvMIrYYaIva0VCq0CoDN7Z3hsRThEYVExI/qoFHnAAxrhG+Wd7Q== +typedoc@^0.27.7: + version "0.27.7" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.27.7.tgz#09047ffb5c845f45765de26c68b77260867fe967" + integrity sha512-K/JaUPX18+61W3VXek1cWC5gwmuLvYTOXJzBvD9W7jFvbPnefRnCHQCEPw7MSNrP/Hj7JJrhZtDDLKdcYm6ucg== dependencies: + "@gerrit0/mini-shiki" "^1.24.0" lunr "^2.3.9" markdown-it "^14.1.0" - minimatch "^9.0.4" - shiki "^1.9.0" - yaml "^2.4.5" + minimatch "^9.0.5" + yaml "^2.6.1" typescript@^5.4.5: version "5.4.5" @@ -4468,10 +4498,10 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@^2.4.5: - version "2.4.5" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.5.tgz#60630b206dd6d84df97003d33fc1ddf6296cca5e" - integrity sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg== +yaml@^2.6.1: + version "2.7.0" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.7.0.tgz#aef9bb617a64c937a9a748803786ad8d3ffe1e98" + integrity sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA== yargs-parser@^20.2.3: version "20.2.9" From f3403eab48e683b71840acc3d2b06bf7525ec433 Mon Sep 17 00:00:00 2001 From: Tat Dat Duong Date: Thu, 13 Feb 2025 07:55:46 -0800 Subject: [PATCH 04/11] Add link ref --- docs/mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index cf945b3d3..47c9f1f6b 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -253,6 +253,7 @@ nav: - cloud/how-tos/stream_events.md - cloud/how-tos/stream_debug.md - cloud/how-tos/stream_multiple.md + - cloud/how-tos/use_stream_react.md - Human-in-the-loop: - Human-in-the-loop: how-tos#human-in-the-loop_1 - cloud/how-tos/human_in_the_loop_breakpoint.md From 2c66ac869db48c4d7299647d59dfd67f88d62de7 Mon Sep 17 00:00:00 2001 From: Tat Dat Duong Date: Thu, 13 Feb 2025 10:17:42 -0800 Subject: [PATCH 05/11] feat(sdk-js): expose branches --- libs/sdk-js/src/react/stream.tsx | 259 +++++++++++++++++-------------- 1 file changed, 139 insertions(+), 120 deletions(-) diff --git a/libs/sdk-js/src/react/stream.tsx b/libs/sdk-js/src/react/stream.tsx index 88190be46..cc3b7f9be 100644 --- a/libs/sdk-js/src/react/stream.tsx +++ b/libs/sdk-js/src/react/stream.tsx @@ -135,6 +135,123 @@ export type MessageMetadata> = { branchOptions: string[] | undefined; }; +function getBranchSequence>( + history: ThreadState[], +) { + const childrenMap: Record[]> = {}; + + // First pass - collect nodes for each checkpoint + history.forEach((state) => { + const checkpointId = state.parent_checkpoint?.checkpoint_id ?? "$"; + childrenMap[checkpointId] ??= []; + childrenMap[checkpointId].push(state); + }); + + // Second pass - create a tree of sequences + type Task = { id: string; sequence: Sequence; path: string[] }; + const rootSequence: Sequence = { type: "sequence", items: [] }; + const queue: Task[] = [{ id: "$", sequence: rootSequence, path: [] }]; + + const paths: string[][] = []; + + const visited = new Set(); + while (queue.length > 0) { + const task = queue.shift()!; + if (visited.has(task.id)) continue; + visited.add(task.id); + + const children = childrenMap[task.id]; + if (children == null || children.length === 0) continue; + + // If we've encountered a fork (2+ children), push the fork + // to the sequence and add a new sequence for each child + let fork: Fork | undefined; + if (children.length > 1) { + fork = { type: "fork", items: [] }; + task.sequence.items.push(fork); + } + + for (const value of children) { + const id = value.checkpoint.checkpoint_id!; + + let sequence = task.sequence; + let path = task.path; + if (fork != null) { + sequence = { type: "sequence", items: [] }; + fork.items.unshift(sequence); + + path = path.slice(); + path.push(id); + paths.push(path); + } + + sequence.items.push({ type: "node", value, path }); + queue.push({ id, sequence, path }); + } + } + + return { rootSequence, paths }; +} + +const PATH_SEP = ">"; +const ROOT_ID = "$"; + +// Get flat view +function getBranchView>( + sequence: Sequence, + paths: string[][], + branch: string, +) { + const path = branch.split(PATH_SEP); + const pathMap: Record = {}; + + for (const path of paths) { + const parent = path.at(-2) ?? ROOT_ID; + pathMap[parent] ??= []; + pathMap[parent].unshift(path); + } + + const history: ThreadState[] = []; + const branchByCheckpoint: Record< + string, + { branch: string | undefined; branchOptions: string[] | undefined } + > = {}; + + const forkStack = path.slice(); + const queue: (Node | Fork)[] = [...sequence.items]; + + while (queue.length > 0) { + const item = queue.shift()!; + + if (item.type === "node") { + history.push(item.value); + branchByCheckpoint[item.value.checkpoint.checkpoint_id!] = { + branch: item.path.join(PATH_SEP), + branchOptions: (item.path.length > 0 + ? pathMap[item.path.at(-2) ?? ROOT_ID] ?? [] + : [] + ).map((p) => p.join(PATH_SEP)), + }; + } + if (item.type === "fork") { + const forkId = forkStack.shift(); + const index = + forkId != null + ? item.items.findIndex((value) => { + const firstItem = value.items.at(0); + if (!firstItem || firstItem.type !== "node") return false; + return firstItem.value.checkpoint.checkpoint_id === forkId; + }) + : -1; + + const nextItems = item.items.at(index)?.items ?? []; + queue.push(...nextItems); + } + } + + return { history, branchByCheckpoint }; +} + function fetchHistory>( client: Client, threadId: string, @@ -274,9 +391,8 @@ export function useStream< ); const [threadId, onThreadId] = useControllableThreadId(options); - const [branchPath, setBranchPath] = useState([]); + const [branch, setBranch] = useState(""); const [isLoading, setIsLoading] = useState(false); - const [_, setEvents] = useState([]); const [streamError, setStreamError] = useState(undefined); const [streamValues, setStreamValues] = useState(null); @@ -330,111 +446,14 @@ export function useStream< : []; }, [messagesKey]); - const [sequence, pathMap] = (() => { - const childrenMap: Record[]> = {}; + const { rootSequence, paths } = getBranchSequence(history.data); + const { history: flatHistory, branchByCheckpoint } = getBranchView( + rootSequence, + paths, + branch, + ); - // First pass - collect nodes for each checkpoint - history.data.forEach((state) => { - const checkpointId = state.parent_checkpoint?.checkpoint_id ?? "$"; - childrenMap[checkpointId] ??= []; - childrenMap[checkpointId].push(state); - }); - - // Second pass - create a tree of sequences - type Task = { id: string; sequence: Sequence; path: string[] }; - const rootSequence: Sequence = { type: "sequence", items: [] }; - const queue: Task[] = [{ id: "$", sequence: rootSequence, path: [] }]; - - const paths: string[][] = []; - - const visited = new Set(); - while (queue.length > 0) { - const task = queue.shift()!; - if (visited.has(task.id)) continue; - visited.add(task.id); - - const children = childrenMap[task.id]; - if (children == null || children.length === 0) continue; - - // If we've encountered a fork (2+ children), push the fork - // to the sequence and add a new sequence for each child - let fork: Fork | undefined; - if (children.length > 1) { - fork = { type: "fork", items: [] }; - task.sequence.items.push(fork); - } - - for (const value of children) { - const id = value.checkpoint.checkpoint_id!; - - let sequence = task.sequence; - let path = task.path; - if (fork != null) { - sequence = { type: "sequence", items: [] }; - fork.items.unshift(sequence); - - path = path.slice(); - path.push(id); - paths.push(path); - } - - sequence.items.push({ type: "node", value, path }); - queue.push({ id, sequence, path }); - } - } - - // Third pass, create a map for available forks - const pathMap: Record = {}; - for (const path of paths) { - const parent = path.at(-2) ?? "$"; - pathMap[parent] ??= []; - pathMap[parent].unshift(path); - } - - return [rootSequence as ValidSequence, pathMap]; - })(); - - const [flatValues, flatPaths] = (() => { - const result: ThreadState[] = []; - const flatPaths: Record< - string, - { current: string[] | undefined; branches: string[][] | undefined } - > = {}; - - const forkStack = branchPath.slice(); - const queue: (Node | Fork)[] = [...sequence.items]; - - while (queue.length > 0) { - const item = queue.shift()!; - - if (item.type === "node") { - result.push(item.value); - flatPaths[item.value.checkpoint.checkpoint_id!] = { - current: item.path, - branches: - item.path.length > 0 ? pathMap[item.path.at(-2) ?? "$"] ?? [] : [], - }; - } - if (item.type === "fork") { - const forkId = forkStack.shift(); - const index = - forkId != null - ? item.items.findIndex((value) => { - const firstItem = value.items.at(0); - if (!firstItem || firstItem.type !== "node") return false; - return firstItem.value.checkpoint.checkpoint_id === forkId; - }) - : -1; - - const nextItems = item.items.at(index)?.items ?? []; - queue.push(...nextItems); - } - } - - return [result, flatPaths]; - })(); - - const threadHead: ThreadState | undefined = flatValues.at(-1); + const threadHead: ThreadState | undefined = flatHistory.at(-1); const historyValues = threadHead?.values ?? ({} as StateType); const historyError = (() => { const error = threadHead?.tasks?.at(-1)?.error; @@ -470,13 +489,13 @@ export function useStream< | undefined; let branch = firstSeen - ? flatPaths[firstSeen.checkpoint.checkpoint_id!] + ? branchByCheckpoint[firstSeen.checkpoint.checkpoint_id!] : undefined; - if (!branch?.current?.length) branch = undefined; + if (!branch?.branch?.length) branch = undefined; // serialize branches - const optionsShown = branch?.branches?.flat(2).join(","); + const optionsShown = branch?.branchOptions?.flat(2).join(","); if (optionsShown) { if (alreadyShown.has(optionsShown)) branch = undefined; alreadyShown.add(optionsShown); @@ -485,8 +504,9 @@ export function useStream< return { messageId: messageId.toString(), firstSeenState: firstSeen, - branch: branch?.current?.join(">"), - branchOptions: branch?.branches?.map((b) => b.join(">")), + + branch: branch?.branch, + branchOptions: branch?.branchOptions, }; }, ); @@ -561,9 +581,10 @@ export function useStream< // Unbranch things const newPath = submitOptions?.checkpoint?.checkpoint_id - ? flatPaths[submitOptions?.checkpoint?.checkpoint_id]?.current + ? branchByCheckpoint[submitOptions?.checkpoint?.checkpoint_id]?.branch : undefined; - if (newPath != null) setBranchPath(newPath ?? []); + + if (newPath != null) setBranch(newPath ?? ""); // Assumption: we're setting the initial value // Used for instant feedback @@ -584,8 +605,6 @@ export function useStream< let streamError: StreamError | undefined; for await (const { event, data } of run) { - setEvents((events) => [...events, { event, data } as EventStreamEvent]); - if (event === "error") { streamError = new StreamError(data); break; @@ -656,11 +675,6 @@ export function useStream< const error = isLoading ? streamError : historyError; const values = streamValues ?? historyValues; - const setBranch = useCallback( - (path: string) => setBranchPath(path.split(">")), - [setBranchPath], - ); - return { get values() { trackStreamMode("values"); @@ -672,8 +686,13 @@ export function useStream< stop, submit, + + branch, setBranch, + history: flatHistory, + experimental_branchTree: rootSequence, + get messages() { trackStreamMode("messages-tuple"); From 422b2ba7f079f543bbc45ad2053150743767199d Mon Sep 17 00:00:00 2001 From: Tat Dat Duong Date: Thu, 13 Feb 2025 10:18:09 -0800 Subject: [PATCH 06/11] Update docs --- docs/docs/cloud/how-tos/use_stream_react.md | 2 +- docs/docs/how-tos/index.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/docs/cloud/how-tos/use_stream_react.md b/docs/docs/cloud/how-tos/use_stream_react.md index 66a7fb9ed..31875301a 100644 --- a/docs/docs/cloud/how-tos/use_stream_react.md +++ b/docs/docs/cloud/how-tos/use_stream_react.md @@ -1,4 +1,4 @@ -# How to Stream LangGraph Runs in Your React App +# How to stream runs in your React frontend !!! info "Prerequisites" - [LangGraph Platform](../concepts/langgraph_platform.md) diff --git a/docs/docs/how-tos/index.md b/docs/docs/how-tos/index.md index 03dd84012..9556afd64 100644 --- a/docs/docs/how-tos/index.md +++ b/docs/docs/how-tos/index.md @@ -204,6 +204,7 @@ Learn how to set up your app for deployment to LangGraph Platform: - [How to test locally](../cloud/deployment/test_locally.md) - [How to rebuild graph at runtime](../cloud/deployment/graph_rebuild.md) - [How to use LangGraph Platform to deploy CrewAI, AutoGen, and other frameworks](autogen-langgraph-platform.ipynb) +- [How to stream runs in your React frontend](../cloud/how-tos/use_stream_react.md) ### Deployment From 7e4852373d676d89c6cb17532c6f5c88303585d6 Mon Sep 17 00:00:00 2001 From: Tat Dat Duong Date: Thu, 13 Feb 2025 10:23:11 -0800 Subject: [PATCH 07/11] Fix broken links --- docs/docs/cloud/how-tos/use_stream_react.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/docs/cloud/how-tos/use_stream_react.md b/docs/docs/cloud/how-tos/use_stream_react.md index 31875301a..9b64f9eb4 100644 --- a/docs/docs/cloud/how-tos/use_stream_react.md +++ b/docs/docs/cloud/how-tos/use_stream_react.md @@ -1,8 +1,8 @@ # How to stream runs in your React frontend !!! info "Prerequisites" - - [LangGraph Platform](../concepts/langgraph_platform.md) - - [LangGraph Server](../concepts/langgraph_server.md) + - [LangGraph Platform](../../concepts/langgraph_platform.md) + - [LangGraph Server](../../concepts/langgraph_server.md) The `useStream()` React hook provides a seamless way to integrate LangGraph runs into your React applications. It handles all the complexities of streaming, state management, and branching logic, letting you focus on building great chat experiences. @@ -400,7 +400,7 @@ The `useStream()` hook provides several callback options to help you respond to - `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. +- `onCustomEvent`: Called when a custom event is received. See [Custom events](../../concepts/streaming.md#custom) to learn how to stream custom events. - `onMetadataEvent`: Called when a metadata event is received. ## Learn More From 3c0a677c90b628649ded13e0243949e587fc34ea Mon Sep 17 00:00:00 2001 From: Tat Dat Duong Date: Thu, 13 Feb 2025 10:36:22 -0800 Subject: [PATCH 08/11] feat(sdk-js): make "messages" the default key --- libs/sdk-js/src/react/stream.tsx | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/libs/sdk-js/src/react/stream.tsx b/libs/sdk-js/src/react/stream.tsx index 88190be46..494ca7408 100644 --- a/libs/sdk-js/src/react/stream.tsx +++ b/libs/sdk-js/src/react/stream.tsx @@ -224,6 +224,9 @@ export function useStream< /** * Specify the key within the state that contains messages. + * Defaults to "messages". + * + * @default "messages" */ messagesKey?: string; @@ -267,7 +270,9 @@ export function useStream< | ErrorStreamEvent | FeedbackStreamEvent; - const { assistantId, messagesKey, onError, onFinish } = options; + let { assistantId, messagesKey, onError, onFinish } = options; + messagesKey ??= "messages"; + const client = useMemo( () => new Client({ apiUrl: options.apiUrl, apiKey: options.apiKey }), [options.apiKey, options.apiUrl], @@ -323,7 +328,6 @@ export function useStream< ); const getMessages = useMemo(() => { - if (messagesKey == null) return undefined; return (value: StateType) => Array.isArray(value[messagesKey]) ? (value[messagesKey] as Message[]) @@ -453,8 +457,6 @@ export function useStream< })(); const messageMetadata = (() => { - if (getMessages == null) return undefined; - const alreadyShown = new Set(); return getMessages(historyValues).map( (message, idx): MessageMetadata => { @@ -597,8 +599,6 @@ export function useStream< if (event === "values") setStreamValues(data); if (event === "messages") { - if (!getMessages) continue; - const [serialized] = data; const messageId = messageManagerRef.current.add(serialized); @@ -676,13 +676,6 @@ export function useStream< get messages() { trackStreamMode("messages-tuple"); - - if (getMessages == null) { - throw new Error( - "No messages key provided. Make sure that `useStream` contains the `messagesKey` property.", - ); - } - return getMessages(values); }, @@ -691,13 +684,6 @@ export function useStream< index?: number, ): MessageMetadata | undefined { trackStreamMode("messages-tuple"); - - if (getMessages == null) { - throw new Error( - "No messages key provided. Make sure that `useStream` contains the `messagesKey` property.", - ); - } - return messageMetadata?.find( (m) => m.messageId === (message.id ?? index), ); From 7ec8a4cb4d4edbcf48399fadc7e491a702d028e6 Mon Sep 17 00:00:00 2001 From: Tat Dat Duong Date: Thu, 13 Feb 2025 10:57:00 -0800 Subject: [PATCH 09/11] Update type definitions --- libs/sdk-js/src/react/stream.tsx | 142 ++++++++++++++++++++++++++----- 1 file changed, 123 insertions(+), 19 deletions(-) diff --git a/libs/sdk-js/src/react/stream.tsx b/libs/sdk-js/src/react/stream.tsx index 963aec843..9919dfbc5 100644 --- a/libs/sdk-js/src/react/stream.tsx +++ b/libs/sdk-js/src/react/stream.tsx @@ -128,10 +128,25 @@ interface ValidSequence { } export type MessageMetadata> = { + /** + * The ID of the message used. + */ messageId: string; + + /** + * The first thread state the message was seen in. + */ firstSeenState: ThreadState | undefined; + /** + * The branch of the message. + */ branch: string | undefined; + + /** + * The list of branches this message is part of. + * This is useful for displaying branching controls. + */ branchOptions: string[] | undefined; }; @@ -319,11 +334,11 @@ const useControllableThreadId = (options?: { return [options.threadId, onThreadId]; }; -export function useStream< +interface UseStreamOptions< StateType extends Record = Record, UpdateType extends Record = Partial, CustomType = unknown, ->(options: { +> { /** * The ID of the assistant to use. */ @@ -372,9 +387,113 @@ export function useStream< */ onMetadataEvent?: (data: MetadataStreamEvent["data"]) => void; + /** + * The ID of the thread to fetch history and current values from. + */ threadId?: string | null; + + /** + * Callback that is called when the thread ID is updated (ie when a new thread is created). + */ onThreadId?: (threadId: string) => void; -}) { +} + +interface UseStream< + StateType extends Record = Record, + UpdateType extends Record = Partial, +> { + /** + * The current values of the thread. + */ + values: StateType; + + /** + * Last seen error from the thread or during streaming. + */ + error: unknown; + + /** + * Whether the stream is currently running. + */ + isLoading: boolean; + + /** + * Stops the stream. + */ + stop: () => void; + + /** + * Create and stream a run to the thread. + */ + submit: (values: UpdateType, options?: SubmitOptions) => void; + + /** + * The current branch of the thread. + */ + branch: string; + + /** + * Set the branch of the thread. + */ + setBranch: (branch: string) => void; + + /** + * Flattened history of thread states of a thread. + */ + history: ThreadState[]; + + /** + * Tree of all branches for the thread. + * @experimental + */ + experimental_branchTree: Sequence; + + /** + * Messages inferred from the thread. + * Will automatically update with incoming message chunks. + */ + messages: Message[]; + + /** + * Get the metadata for a message, such as first thread state the message + * was seen in and branch information. + + * @param message - The message to get the metadata for. + * @param index - The index of the message in the thread. + * @returns The metadata for the message. + */ + getMessagesMetadata: ( + message: Message, + index?: number, + ) => MessageMetadata | undefined; +} + +interface SubmitOptions< + StateType extends Record = Record, +> { + config?: Config; + checkpoint?: Omit | null; + command?: Command; + interruptBefore?: "*" | string[]; + interruptAfter?: "*" | string[]; + metadata?: Metadata; + multitaskStrategy?: MultitaskStrategy; + onCompletion?: OnCompletionBehavior; + onDisconnect?: DisconnectMode; + feedbackKeys?: string[]; + streamMode?: Array; + optimisticValues?: + | Partial + | ((prev: StateType) => Partial); +} + +export function useStream< + StateType extends Record = Record, + UpdateType extends Record = Partial, + CustomType = unknown, +>( + options: UseStreamOptions, +): UseStream { type EventStreamEvent = | ValuesStreamEvent | UpdatesStreamEvent @@ -521,22 +640,7 @@ export function useStream< const submit = async ( values: UpdateType | undefined, - submitOptions?: { - config?: Config; - checkpoint?: Omit | null; - command?: Command; - interruptBefore?: "*" | string[]; - interruptAfter?: "*" | string[]; - metadata?: Metadata; - multitaskStrategy?: MultitaskStrategy; - onCompletion?: OnCompletionBehavior; - onDisconnect?: DisconnectMode; - feedbackKeys?: string[]; - streamMode?: Array; - optimisticValues?: - | Partial - | ((prev: StateType) => Partial); - }, + submitOptions?: SubmitOptions, ) => { try { setIsLoading(true); From d2ab02edf19c88a829cf5cfe4a9655b8176bd064 Mon Sep 17 00:00:00 2001 From: Tat Dat Duong Date: Thu, 13 Feb 2025 11:15:45 -0800 Subject: [PATCH 10/11] Add shoutout to CopilotKit and assistant-ui --- docs/docs/cloud/how-tos/use_stream_react.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/docs/cloud/how-tos/use_stream_react.md b/docs/docs/cloud/how-tos/use_stream_react.md index 9b64f9eb4..89fd8482a 100644 --- a/docs/docs/cloud/how-tos/use_stream_react.md +++ b/docs/docs/cloud/how-tos/use_stream_react.md @@ -1,10 +1,10 @@ -# How to stream runs in your React frontend +# How to integrate LangGraph into your React application !!! info "Prerequisites" - [LangGraph Platform](../../concepts/langgraph_platform.md) - [LangGraph Server](../../concepts/langgraph_server.md) -The `useStream()` React hook provides a seamless way to integrate LangGraph runs into your React applications. It handles all the complexities of streaming, state management, and branching logic, letting you focus on building great chat experiences. +The `useStream()` React hook provides a seamless way to integrate LangGraph into your React applications. It handles all the complexities of streaming, state management, and branching logic, letting you focus on building great chat experiences. Key features: @@ -15,6 +15,8 @@ Key features: Let's explore how to use `useStream()` in your React application. +The `useStream()` provides a solid foundation for creating bespoke chat experiences. For pre-built chat components and interfaces, we recommend checking out [CopilotKit](https://docs.copilotkit.ai/coagents/quickstart/langgraph) and [assistant-ui](https://github.com/langchain-ai/assistant-ui). + ## Example ```tsx From ce6b396186270433ba6a3e85eec6118c2f5c2f3a Mon Sep 17 00:00:00 2001 From: Tat Dat Duong Date: Thu, 13 Feb 2025 11:16:32 -0800 Subject: [PATCH 11/11] Update index page as well --- docs/docs/how-tos/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/how-tos/index.md b/docs/docs/how-tos/index.md index 9556afd64..45c47f0c7 100644 --- a/docs/docs/how-tos/index.md +++ b/docs/docs/how-tos/index.md @@ -204,7 +204,7 @@ Learn how to set up your app for deployment to LangGraph Platform: - [How to test locally](../cloud/deployment/test_locally.md) - [How to rebuild graph at runtime](../cloud/deployment/graph_rebuild.md) - [How to use LangGraph Platform to deploy CrewAI, AutoGen, and other frameworks](autogen-langgraph-platform.ipynb) -- [How to stream runs in your React frontend](../cloud/how-tos/use_stream_react.md) +- [How to integrate LangGraph into your React application](../cloud/how-tos/use_stream_react.md) ### Deployment