Skip to content

Commit df77f0a

Browse files
authored
Merge pull request #72 from CopilotKit/docs/usage-guide
docs: add usage and integration guide
2 parents 341411d + 5f32aea commit df77f0a

File tree

10 files changed

+1223
-0
lines changed

10 files changed

+1223
-0
lines changed

docs/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Open Generative UI Documentation
2+
3+
Open Generative UI is a showcase and template for building AI agents with [CopilotKit](https://copilotkit.ai) and [LangGraph](https://langchain-ai.github.io/langgraph/). It demonstrates agent-driven UI where an AI agent and users collaboratively manipulate shared application state.
4+
5+
## Prerequisites
6+
7+
- Node.js 22+
8+
- Python 3.12+
9+
- [pnpm](https://pnpm.io/) 9+
10+
- [uv](https://docs.astral.sh/uv/) (Python package manager)
11+
- An OpenAI API key
12+
13+
## Documentation
14+
15+
| Guide | Description |
16+
|-------|-------------|
17+
| [Getting Started](getting-started.md) | Install, configure, and run the project |
18+
| [Architecture](architecture.md) | How the monorepo and request flow are structured |
19+
| [Agent State](agent-state.md) | Bidirectional state sync between agent and frontend |
20+
| [Generative UI](generative-ui.md) | Register React components the agent can render |
21+
| [Agent Tools](agent-tools.md) | Create Python tools that read and update state |
22+
| [Human in the Loop](human-in-the-loop.md) | Pause the agent to collect user input |
23+
| [MCP Integration](mcp-integration.md) | Optional Model Context Protocol server |
24+
| [Deployment](deployment.md) | Deploy to Render or other platforms |
25+
| [Bring to Your App](bring-to-your-app.md) | Adopt these patterns in your own project |

docs/agent-state.md

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# Agent State
2+
3+
The core pattern in this project is **CopilotKit v2's agent state** — state lives in the LangGraph agent and syncs bidirectionally with the React frontend. Both the user and agent can read and write the same state.
4+
5+
## Define State in the Agent
6+
7+
State is defined as a Python `TypedDict` that extends `BaseAgentState`:
8+
9+
```python
10+
# apps/agent/src/todos.py
11+
12+
from langchain.agents import AgentState as BaseAgentState
13+
from typing import TypedDict, Literal
14+
15+
class Todo(TypedDict):
16+
id: str
17+
title: str
18+
description: str
19+
emoji: str
20+
status: Literal["pending", "completed"]
21+
22+
class AgentState(BaseAgentState):
23+
todos: list[Todo]
24+
```
25+
26+
The state schema is passed to the agent via `context_schema`:
27+
28+
```python
29+
# apps/agent/main.py
30+
31+
agent = create_deep_agent(
32+
model=ChatOpenAI(model="gpt-5.4-2026-03-05"),
33+
tools=[...],
34+
middleware=[CopilotKitMiddleware()],
35+
context_schema=AgentState, # ← state schema
36+
...
37+
)
38+
```
39+
40+
## Read State in React
41+
42+
Use the `useAgent()` hook to access agent state:
43+
44+
```tsx
45+
import { useAgent } from "@copilotkit/react-core/v2";
46+
47+
function MyComponent() {
48+
const { agent } = useAgent();
49+
const todos = agent.state?.todos || [];
50+
51+
return (
52+
<ul>
53+
{todos.map(todo => (
54+
<li key={todo.id}>{todo.emoji} {todo.title}</li>
55+
))}
56+
</ul>
57+
);
58+
}
59+
```
60+
61+
## Write State from React
62+
63+
Call `agent.setState()` to update state from the frontend:
64+
65+
```tsx
66+
const toggleTodo = (todoId: string) => {
67+
const updated = todos.map(t =>
68+
t.id === todoId
69+
? { ...t, status: t.status === "completed" ? "pending" : "completed" }
70+
: t
71+
);
72+
agent.setState({ todos: updated });
73+
};
74+
75+
const addTodo = () => {
76+
const newTodo = {
77+
id: crypto.randomUUID(),
78+
title: "New task",
79+
description: "",
80+
emoji: "📝",
81+
status: "pending",
82+
};
83+
agent.setState({ todos: [...todos, newTodo] });
84+
};
85+
```
86+
87+
## Write State from Agent Tools
88+
89+
Tools update state by returning a `Command` with an `update` dict:
90+
91+
```python
92+
# apps/agent/src/todos.py
93+
94+
from langgraph.types import Command
95+
from langchain.tools import tool, ToolRuntime
96+
from langchain.messages import ToolMessage
97+
98+
@tool
99+
def manage_todos(todos: list[Todo], runtime: ToolRuntime) -> Command:
100+
"""Manage the current todos."""
101+
for todo in todos:
102+
if "id" not in todo or not todo["id"]:
103+
todo["id"] = str(uuid.uuid4())
104+
105+
return Command(update={
106+
"todos": todos,
107+
"messages": [
108+
ToolMessage(
109+
content="Successfully updated todos",
110+
tool_call_id=runtime.tool_call_id
111+
)
112+
]
113+
})
114+
```
115+
116+
## Read State in Agent Tools
117+
118+
Use `runtime.state` to read current state:
119+
120+
```python
121+
@tool
122+
def get_todos(runtime: ToolRuntime):
123+
"""Get the current todos."""
124+
return runtime.state.get("todos", [])
125+
```
126+
127+
## How State Flows
128+
129+
1. **User edits a todo**`agent.setState({ todos: [...] })`
130+
2. **CopilotKit syncs** the change to the agent backend
131+
3. **Agent observes** the updated state via `runtime.state`
132+
4. **Agent calls a tool**`Command(update={ "todos": [...] })`
133+
5. **CopilotKit syncs** the update back to the frontend
134+
6. **React re-renders** because `agent.state.todos` changed
135+
136+
The key insight: there is no separate frontend state management. State lives in the agent, and CopilotKit handles the sync.
137+
138+
## Adding New State Fields
139+
140+
To add a new field to agent state:
141+
142+
1. Add the field to `AgentState` in Python:
143+
```python
144+
class AgentState(BaseAgentState):
145+
todos: list[Todo]
146+
tags: list[str] # new field
147+
```
148+
149+
2. Read it in React:
150+
```tsx
151+
const tags = agent.state?.tags || [];
152+
```
153+
154+
3. Write it from React or tools the same way as above.

docs/agent-tools.md

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# Agent Tools
2+
3+
Tools are Python functions that the LangGraph agent can call. They can read and update agent state, fetch data, and perform any server-side logic.
4+
5+
## Creating a Tool
6+
7+
Use the `@tool` decorator from LangChain:
8+
9+
```python
10+
from langchain.tools import tool, ToolRuntime
11+
12+
@tool
13+
def my_tool(arg1: str, arg2: int, runtime: ToolRuntime):
14+
"""Description of what this tool does. The agent reads this to decide when to call it."""
15+
return f"Result: {arg1} x {arg2}"
16+
```
17+
18+
- The docstring tells the agent when and how to use the tool
19+
- `runtime: ToolRuntime` gives access to agent state (optional parameter)
20+
- Return value is sent back to the agent as the tool result
21+
22+
## Reading State
23+
24+
Access current agent state via `runtime.state`:
25+
26+
```python
27+
# apps/agent/src/todos.py
28+
29+
@tool
30+
def get_todos(runtime: ToolRuntime):
31+
"""Get the current todos."""
32+
return runtime.state.get("todos", [])
33+
```
34+
35+
## Updating State
36+
37+
Return a `Command` with an `update` dict to modify agent state:
38+
39+
```python
40+
from langgraph.types import Command
41+
from langchain.messages import ToolMessage
42+
43+
@tool
44+
def manage_todos(todos: list[Todo], runtime: ToolRuntime) -> Command:
45+
"""Manage the current todos."""
46+
# Ensure all todos have unique IDs
47+
for todo in todos:
48+
if "id" not in todo or not todo["id"]:
49+
todo["id"] = str(uuid.uuid4())
50+
51+
return Command(update={
52+
"todos": todos,
53+
"messages": [
54+
ToolMessage(
55+
content="Successfully updated todos",
56+
tool_call_id=runtime.tool_call_id
57+
)
58+
]
59+
})
60+
```
61+
62+
Key points:
63+
- `Command(update={...})` merges the update into agent state
64+
- Include a `ToolMessage` in the `messages` list to acknowledge the tool call
65+
- Use `runtime.tool_call_id` for the message's `tool_call_id`
66+
67+
## Returning Data (No State Update)
68+
69+
For tools that just return data without modifying state, return the value directly:
70+
71+
```python
72+
# apps/agent/src/query.py
73+
74+
import csv
75+
from pathlib import Path
76+
from langchain.tools import tool
77+
78+
# Load data at module init
79+
_data = []
80+
with open(Path(__file__).parent / "db.csv") as f:
81+
_data = list(csv.DictReader(f))
82+
83+
@tool
84+
def query_data(query: str):
85+
"""Query the financial transactions database. Call this before creating charts."""
86+
return _data
87+
```
88+
89+
## Registering Tools with the Agent
90+
91+
Add tools to the agent's `tools` list in `apps/agent/main.py`:
92+
93+
```python
94+
from src.todos import todo_tools # [manage_todos, get_todos]
95+
from src.query import query_data
96+
from src.plan import plan_visualization
97+
98+
agent = create_deep_agent(
99+
model=ChatOpenAI(model="gpt-5.4-2026-03-05"),
100+
tools=[query_data, plan_visualization, *todo_tools],
101+
middleware=[CopilotKitMiddleware()],
102+
context_schema=AgentState,
103+
...
104+
)
105+
```
106+
107+
You can pass individual tools or spread a list of tools.
108+
109+
## Example: Adding a New Tool
110+
111+
Say you want to add a tool that fetches weather data:
112+
113+
**1. Create the tool** (`apps/agent/src/weather.py`):
114+
115+
```python
116+
from langchain.tools import tool
117+
118+
@tool
119+
def get_weather(city: str):
120+
"""Get the current weather for a city."""
121+
# Your implementation here
122+
return {"city": city, "temp": 72, "condition": "sunny"}
123+
```
124+
125+
**2. Register it** in `apps/agent/main.py`:
126+
127+
```python
128+
from src.weather import get_weather
129+
130+
agent = create_deep_agent(
131+
tools=[query_data, plan_visualization, *todo_tools, get_weather],
132+
...
133+
)
134+
```
135+
136+
The agent can now call `get_weather` when a user asks about weather. If you want a custom UI for the result, register a `useRenderTool` on the frontend (see [Generative UI](generative-ui.md)).
137+
138+
## Next Steps
139+
140+
- [Agent State](agent-state.md) — How state sync works between tools and the frontend
141+
- [Generative UI](generative-ui.md) — Render custom UI for tool results

0 commit comments

Comments
 (0)