Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/content/docs/chat/providers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,27 @@ Use the same decision rules:
- start with `apiUrl` when the endpoint already matches the request and stream shape your frontend expects
- switch to `processMessage` when you need auth headers, a custom body, dynamic routing, or provider-specific metadata

`@openuidev/react-headless` ships `langGraphAdapter()` and `langGraphMessageFormat` for exactly this. Pair them with a `processMessage` that posts to a proxy route, converting messages with `langGraphMessageFormat.toApi`:

```tsx
import { langGraphAdapter, langGraphMessageFormat } from "@openuidev/react-headless";
import { FullScreen } from "@openuidev/react-ui";

<FullScreen
processMessage={async ({ messages, abortController }) =>
fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ messages: langGraphMessageFormat.toApi(messages) }),
signal: abortController.signal,
})
}
streamProtocol={langGraphAdapter()}
/>;
```

For a complete, runnable version (including a multi-agent supervisor graph and the server-side proxy that hides your API key), see the [LangGraph Chat example](/docs/openui-lang/examples/langgraph-chat).

{/* add visual: flow-chart showing provider choice splitting first by emitted stream format, then by whether messageFormat is needed */}

## Related guides
Expand Down
188 changes: 188 additions & 0 deletions docs/content/docs/openui-lang/examples/langgraph-chat.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
---
title: LangGraph Chat
description: A multi-agent LangGraph supervisor that routes each message to a weather, finance, or research specialist and streams OpenUI Lang into live generative UI.
---

OpenUI's renderer is transport-agnostic: it turns a stream of OpenUI Lang markup into interactive React components no matter how that stream is produced. This example produces it with a **multi-agent [LangGraph](https://langchain-ai.github.io/langgraphjs/) graph**: a supervisor routes each user message to one of three specialists (**weather**, **finance**, or **research**), and the chosen agent streams its answer as OpenUI Lang, which `<FullScreen>` renders into cards, tables, and charts as the tokens arrive.

Because every specialist shares the same OpenUI system prompt (generated from the component library), any agent you add automatically speaks generative UI.

[View source on GitHub →](https://github.com/thesysdev/openui/tree/main/examples/langgraph-chat)

<div className="bg-[rgba(0,0,0,0.05)] dark:bg-gray-900 rounded-2xl h-[500px] flex p-2">
<video
src="/videos/langgraph-chat.mp4"
noControls
playsInline
muted
preload="metadata"
className="w-full rounded-lg m-auto"
autoPlay
loop
/>
</div>

## Architecture

```
browser ──fetch /api/chat──▶ Next.js route ──@langchain/langgraph-sdk──▶ LangGraph server
▲ │ (router → specialist
└────── SSE (LangGraph) ───────┘ → tools → OpenUI Lang)
parsed by langGraphAdapter()
```

The example runs **two processes**: the LangGraph server runs the graph (and the LLM), and the Next.js app serves the UI. The browser never talks to the LangGraph server directly. A thin Next.js proxy route opens a run, forwards the stream, and keeps the API key and deployment URL server-side.

## Connecting the frontend

The client is a single `<FullScreen>`. `processMessage` posts the conversation to the proxy, and `langGraphAdapter()` parses the LangGraph SSE stream that comes back:

```tsx
import { langGraphAdapter, langGraphMessageFormat } from "@openuidev/react-headless";
import { FullScreen } from "@openuidev/react-ui";
import { openuiChatLibrary } from "@openuidev/react-ui/genui-lib";

<FullScreen
processMessage={async ({ messages, abortController }) =>
fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
// Convert OpenUI messages to LangChain shape. The run is stateless,
// so the full history is sent each turn.
body: JSON.stringify({ messages: langGraphMessageFormat.toApi(messages) }),
signal: abortController.signal,
})
}
streamProtocol={langGraphAdapter()}
componentLibrary={openuiChatLibrary}
agentName="OpenUI + LangGraph Chat"
/>;
```

- `processMessage`: posts the conversation to the proxy; `langGraphMessageFormat.toApi` converts OpenUI messages into the LangChain message shape the graph expects
- `streamProtocol={langGraphAdapter()}`: parses LangGraph's `messages` SSE events back into streaming assistant text
- `componentLibrary={openuiChatLibrary}`: maps OpenUI Lang nodes to the built-in component set (cards, tables, charts, forms)

## The proxy route

The browser posts messages already in LangChain format, and the route opens a **stateless** streaming run against the LangGraph server, forwarding its Server-Sent Events to the browser. Keeping the connection here is what lets you attach the API key and hide the deployment URL.

```tsx
import { Client } from "@langchain/langgraph-sdk";

// Tokens from internal nodes (the supervisor's routing decision) must not
// surface as assistant output.
const INTERNAL_NODES = new Set(["router"]);

const client = new Client({ apiUrl: API_URL, apiKey: API_KEY });

const run = client.runs.stream(null, ASSISTANT_ID, {
input: { messages: visibleMessages },
streamMode: ["messages-tuple", "updates"],
signal: req.signal,
});

for await (const chunk of run) {
if (chunk.event?.startsWith("messages")) {
const meta = Array.isArray(chunk.data) ? chunk.data[1] : undefined;
if (meta?.langgraph_node && INTERNAL_NODES.has(meta.langgraph_node)) continue;
// Normalize the event name to what langGraphAdapter() expects.
send("messages", chunk.data);
}
}
```

The graph streams in `messages-tuple` mode; the proxy filters out the supervisor's tokens and re-emits the rest as `event: messages`, which is exactly what `langGraphAdapter()` consumes.

## The multi-agent graph

Each specialist owns one tool and a short role hint layered on top of the shared OpenUI prompt. The supervisor makes a single structured routing decision, then the chosen specialist runs its own ReAct loop (`agent → tools → agent → … → END`):

```tsx
const SPECIALISTS = {
weather: { tools: [getWeather], hint: "You are the weather specialist. Use get_weather, then present conditions and the forecast as generative UI..." },
finance: { tools: [getStockPrice], hint: "You are the finance specialist. Use get_stock_price, then present the quote and day range as generative UI..." },
research: { tools: [searchWeb], hint: "You are the research specialist. Use search_web, then summarize the findings as generative UI..." },
};

function agentNode(specialist) {
const { tools, hint } = SPECIALISTS[specialist];
const boundModel = chatModel.bindTools(tools);
// Every specialist shares the generated OpenUI prompt. This is what makes
// each agent's streamed output render as generative UI.
const systemMessage = new SystemMessage(`${OPENUI_SYSTEM_PROMPT}\n\n${hint}`);
return async (state) => ({ messages: [await boundModel.invoke([systemMessage, ...state.messages])] });
}

export const graph = new StateGraph(AgentState)
.addNode("router", router)
.addNode("weather_agent", agentNode("weather"))
.addNode("weather_tools", new ToolNode(SPECIALISTS.weather.tools))
// ...finance and research nodes wired the same way
.addEdge(START, "router")
.addConditionalEdges("router", (state) => state.next, {
weather: "weather_agent",
finance: "finance_agent",
research: "research_agent",
})
.compile();
```

The supervisor (`router`) uses `withStructuredOutput` against a small Zod enum to pick exactly one specialist, defaulting to `research` if routing fails. The tools (`get_weather`, `get_stock_price`, `search_web`) return canned-but-plausible JSON, so the example runs without any third-party API keys. Swap the bodies for real API calls when adapting it.

Add a specialist by extending the `SPECIALISTS` map and wiring a matching `*_agent` / `*_tools` node pair; it inherits the OpenUI prompt for free.

## Project layout

```
examples/langgraph-chat/
|- src/app/page.tsx # <FullScreen> wired to langGraphAdapter()
|- src/app/api/chat/route.ts # Stateless proxy to the LangGraph server (SSE)
|- src/agent/graph.ts # Supervisor + specialist ReAct loops
|- src/agent/tools.ts # Mock weather / finance / research tools
|- src/library.ts # The OpenUI components the model can render
|- src/generated/ # Generated OpenUI system prompt
|- langgraph.json # Graph config for `langgraphjs dev` / deploy
```

## Run the example

This example runs **two processes**, so use two terminals. Run these commands from `examples/langgraph-chat`.

1. Install dependencies and create a `.env` from the template:

```bash
cd examples/langgraph-chat
pnpm install
cp .env.example .env
```

Add your `OPENAI_API_KEY` to `.env`. It is read by the **LangGraph server** (which runs the LLM), so it belongs next to `langgraph.json`. The Next.js app only needs the `LANGGRAPH_*` variables, which already default to the local server.

2. **Terminal 1: LangGraph server** (generates the OpenUI prompt, then hot-reloads the graph on `:2024`):

```bash
pnpm langgraph:dev
```

3. **Terminal 2: Next.js app**:

```bash
pnpm dev
```

Open [http://localhost:3000](http://localhost:3000) and try a starter such as "Weather in Tokyo" or "AAPL stock price".

## Deploy to LangGraph Platform

The folder already ships a `langgraph.json`, so you can deploy the graph and point the app at the deployment with no app code changes, just update `.env`:

```bash
LANGGRAPH_API_URL=https://your-deployment.us.langgraph.app
LANGGRAPH_ASSISTANT_ID=agent # graph name, or a created assistant id
LANGSMITH_API_KEY=lsv2-... # auth for the deployment
```

The SDK sends `LANGSMITH_API_KEY` as `x-api-key` from the server side only. Restart `pnpm dev` after changing `.env`.

For the configuration-level decision between `apiUrl` and `processMessage` when wiring any LangGraph backend, see the [Providers guide](/docs/chat/providers#langgraph).
1 change: 1 addition & 0 deletions docs/content/docs/openui-lang/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"examples/shadcn-chat",
"examples/react-native",
"examples/vercel-ai-chat",
"examples/langgraph-chat",
"examples/react-email"
]
}
Binary file added docs/public/videos/langgraph-chat.mp4
Binary file not shown.
Loading