Files
2026-07-13 12:58:18 +08:00

306 lines
9.6 KiB
Markdown

# LangGraph Integration
CopilotKit supports LangGraph in three configurations: Python with self-hosted FastAPI, Python with LangGraph Platform, and JavaScript/TypeScript. All use the AG-UI protocol.
## Python (Self-Hosted FastAPI)
This is the `langgraph-fastapi` example pattern. You run the LangGraph agent as a standalone FastAPI server and connect via `LangGraphHttpAgent`.
### Prerequisites
- Python 3.10+
- Node.js 18+
- OpenAI API key
- `poetry` or `uv` for Python dependency management
### Python Dependencies
```toml
# pyproject.toml
[project]
dependencies = [
"copilotkit==0.1.74",
"langchain==1.0.1",
"langchain-openai==1.0.1",
"langgraph==1.0.1",
"fastapi==0.115.12",
"uvicorn>=0.38.0",
"python-dotenv>=1.0.0",
"ag-ui-langgraph==0.0.22",
"pydantic>=2.0.0,<3.0.0",
]
```
### Agent Definition (agent/src/agent.py)
The agent extends `CopilotKitState` for shared state and uses the standard ReAct pattern:
```python
from copilotkit import CopilotKitState
from langchain.tools import tool
from langchain_core.messages import SystemMessage
from langchain_core.runnables import RunnableConfig
from langchain_openai import ChatOpenAI
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph
from langgraph.prebuilt import ToolNode
from langgraph.types import Command
from typing_extensions import Literal
from src.util import should_route_to_tool_node
class AgentState(CopilotKitState):
proverbs: list[str]
@tool
def get_weather(location: str):
"""Get the weather for a given location."""
return f"The weather for {location} is 70 degrees."
tools = [get_weather]
async def chat_node(
state: AgentState, config: RunnableConfig
) -> Command[Literal["tool_node", "__end__"]]:
model = ChatOpenAI(model="gpt-4o")
# Bind both frontend (CopilotKit) actions and backend tools
fe_tools = state.get("copilotkit", {}).get("actions", [])
model_with_tools = model.bind_tools([*fe_tools, *tools])
system_message = SystemMessage(
content=f"You are a helpful assistant. The current proverbs are {state.get('proverbs', [])}."
)
response = await model_with_tools.ainvoke(
[system_message, *state["messages"]], config,
)
tool_calls = response.tool_calls
if tool_calls and should_route_to_tool_node(tool_calls, fe_tools):
return Command(goto="tool_node", update={"messages": response})
return Command(goto="__end__", update={"messages": response})
workflow = StateGraph(AgentState)
workflow.add_node("chat_node", chat_node)
workflow.add_node("tool_node", ToolNode(tools=tools))
workflow.add_edge("tool_node", "chat_node")
workflow.set_entry_point("chat_node")
graph = workflow.compile(checkpointer=MemorySaver())
```
Key pattern: `CopilotKitState` provides the `copilotkit` field containing `actions` (frontend tools). You must bind both frontend actions and backend tools to the model, then route frontend tool calls back to CopilotKit (not the ToolNode).
### FastAPI Server (agent/main.py)
```python
from fastapi import FastAPI
from copilotkit import LangGraphAGUIAgent
from ag_ui_langgraph import add_langgraph_fastapi_endpoint
from src.agent import graph
app = FastAPI()
add_langgraph_fastapi_endpoint(
app=app,
agent=LangGraphAGUIAgent(
name="sample_agent",
description="An example agent.",
graph=graph,
),
path="/",
)
```
### Next.js Route (src/app/api/copilotkit/[[...slug]]/route.ts)
```typescript
import {
CopilotRuntime,
createCopilotHonoHandler,
InMemoryAgentRunner,
} from "@copilotkit/runtime/v2";
import { LangGraphHttpAgent } from "@copilotkit/runtime/langgraph";
import { handle } from "hono/vercel";
const runtime = new CopilotRuntime({
agents: {
default: new LangGraphHttpAgent({
url: `${process.env.AGENT_URL || "http://localhost:8123"}/`,
}),
},
runner: new InMemoryAgentRunner(),
});
const app = createCopilotHonoHandler({
runtime,
basePath: "/api/copilotkit",
});
export const GET = handle(app);
export const POST = handle(app);
export const PATCH = handle(app);
export const DELETE = handle(app);
```
Use `LangGraphHttpAgent` (from `@copilotkit/runtime/langgraph`) for self-hosted agents -- the FastAPI server runs under `ag-ui-langgraph`, which speaks AG-UI directly. The default port is 8123 (note the trailing slash on the URL).
---
## Python (LangGraph Platform / Monorepo)
This is the `langgraph-python` example pattern. Uses `LangGraphAgent` which connects to a LangGraph deployment (local or cloud).
### Next.js Route (src/app/api/copilotkit/[[...slug]]/route.ts)
```typescript
import {
CopilotRuntime,
createCopilotHonoHandler,
InMemoryAgentRunner,
} from "@copilotkit/runtime/v2";
import { LangGraphAgent } from "@copilotkit/runtime/langgraph";
import { handle } from "hono/vercel";
const defaultAgent = new LangGraphAgent({
deploymentUrl:
process.env.LANGGRAPH_DEPLOYMENT_URL || "http://localhost:8123",
graphId: "sample_agent",
langsmithApiKey: process.env.LANGSMITH_API_KEY || "",
});
const runtime = new CopilotRuntime({
agents: { default: defaultAgent },
runner: new InMemoryAgentRunner(),
});
const app = createCopilotHonoHandler({
runtime,
basePath: "/api/copilotkit",
});
export const GET = handle(app);
export const POST = handle(app);
export const PATCH = handle(app);
export const DELETE = handle(app);
```
Key difference from self-hosted: `LangGraphAgent` uses `deploymentUrl` and `graphId` (and optionally `langsmithApiKey`) to target the LangGraph Platform / `langgraph-cli dev` surface, while `LangGraphHttpAgent` uses a plain `url` for a self-hosted AG-UI server.
---
## JavaScript / TypeScript
This is the `langgraph-js` example pattern. The agent is a TypeScript LangGraph graph running in a separate Node.js process.
### Agent Definition (apps/agent/src/agent.ts)
```typescript
import { z } from "zod";
import { tool } from "@langchain/core/tools";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import { AIMessage, SystemMessage } from "@langchain/core/messages";
import { MemorySaver, START, StateGraph } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import {
convertActionsToDynamicStructuredTools,
CopilotKitStateAnnotation,
} from "@copilotkit/sdk-js/langgraph";
import { Annotation } from "@langchain/langgraph";
const AgentStateAnnotation = Annotation.Root({
...CopilotKitStateAnnotation.spec,
proverbs: Annotation<string[]>,
});
export type AgentState = typeof AgentStateAnnotation.State;
const getWeather = tool(
(args) => `The weather for ${args.location} is 70 degrees.`,
{
name: "getWeather",
description: "Get the weather for a given location.",
schema: z.object({ location: z.string() }),
},
);
const tools = [getWeather];
async function chat_node(state: AgentState, config) {
const model = new ChatOpenAI({ temperature: 0, model: "gpt-4o" });
const modelWithTools = model.bindTools!([
...convertActionsToDynamicStructuredTools(state.copilotkit?.actions ?? []),
...tools,
]);
const systemMessage = new SystemMessage({
content: `You are a helpful assistant. The current proverbs are ${JSON.stringify(state.proverbs)}.`,
});
const response = await modelWithTools.invoke(
[systemMessage, ...state.messages],
config,
);
return { messages: response };
}
function shouldContinue({ messages, copilotkit }: AgentState) {
const lastMessage = messages[messages.length - 1] as AIMessage;
if (lastMessage.tool_calls?.length) {
const actions = copilotkit?.actions;
const toolCallName = lastMessage.tool_calls![0].name;
if (!actions || actions.every((action) => action.name !== toolCallName)) {
return "tool_node";
}
}
return "__end__";
}
const workflow = new StateGraph(AgentStateAnnotation)
.addNode("chat_node", chat_node)
.addNode("tool_node", new ToolNode(tools))
.addEdge(START, "chat_node")
.addEdge("tool_node", "chat_node")
.addConditionalEdges("chat_node", shouldContinue);
export const graph = workflow.compile({ checkpointer: new MemorySaver() });
```
Key JS-specific patterns:
- Use `CopilotKitStateAnnotation` from `@copilotkit/sdk-js/langgraph` to include CopilotKit state
- Use `convertActionsToDynamicStructuredTools()` to convert frontend actions to LangChain tools
- Check `copilotkit.actions` to determine whether a tool call should route to `tool_node` (backend) or `__end__` (frontend)
### Serving the JS graph
`LangGraphAgent` with `deploymentUrl`/`graphId` targets the LangGraph **server** surface, not the bare compiled `graph` export. Serve the graph with the LangGraph JS CLI (`@langchain/langgraph-cli`) so that surface exists. Add a `langgraph.json` next to the agent:
```json
{
"node_version": "20",
"dependencies": ["."],
"graphs": {
"sample_agent": "./src/agent.ts:graph"
},
"env": "../.env"
}
```
Run it with `langgraphjs dev --port 8123` (the agent app's `dev` script). The `graphId` you pass to `LangGraphAgent` must match a key under `graphs` (here `"sample_agent"`), and `deploymentUrl` points at the CLI server (`http://localhost:8123`).
### Next.js Route
The catch-all `src/app/api/copilotkit/[[...slug]]/route.ts` uses `LangGraphAgent` (from `@copilotkit/runtime/langgraph`) with `deploymentUrl` (the `langgraphjs dev` URL, e.g. `http://localhost:8123`) and `graphId` (`"sample_agent"`), mounted via `createCopilotHonoHandler`.
## Monorepo Structure (JS)
The JS variant uses a Turborepo monorepo:
```
apps/
web/ # Next.js frontend
agent/ # LangGraph agent, served via `langgraphjs dev` (langgraph.json)
pnpm-workspace.yaml
turbo.json
```
Run `pnpm dev` to start both apps via Turborepo (the agent app runs `langgraphjs dev --port 8123`).