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
25 changes: 25 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Open Generative UI Documentation

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.

## Prerequisites

- Node.js 22+
- Python 3.12+
- [pnpm](https://pnpm.io/) 9+
- [uv](https://docs.astral.sh/uv/) (Python package manager)
- An OpenAI API key

## Documentation

| Guide | Description |
|-------|-------------|
| [Getting Started](getting-started.md) | Install, configure, and run the project |
| [Architecture](architecture.md) | How the monorepo and request flow are structured |
| [Agent State](agent-state.md) | Bidirectional state sync between agent and frontend |
| [Generative UI](generative-ui.md) | Register React components the agent can render |
| [Agent Tools](agent-tools.md) | Create Python tools that read and update state |
| [Human in the Loop](human-in-the-loop.md) | Pause the agent to collect user input |
| [MCP Integration](mcp-integration.md) | Optional Model Context Protocol server |
| [Deployment](deployment.md) | Deploy to Render or other platforms |
| [Bring to Your App](bring-to-your-app.md) | Adopt these patterns in your own project |
154 changes: 154 additions & 0 deletions docs/agent-state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# Agent State

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.

## Define State in the Agent

State is defined as a Python `TypedDict` that extends `BaseAgentState`:

```python
# apps/agent/src/todos.py

from langchain.agents import AgentState as BaseAgentState
from typing import TypedDict, Literal

class Todo(TypedDict):
id: str
title: str
description: str
emoji: str
status: Literal["pending", "completed"]

class AgentState(BaseAgentState):
todos: list[Todo]
```

The state schema is passed to the agent via `context_schema`:

```python
# apps/agent/main.py

agent = create_deep_agent(
model=ChatOpenAI(model="gpt-5.4-2026-03-05"),
tools=[...],
middleware=[CopilotKitMiddleware()],
context_schema=AgentState, # ← state schema
...
)
```

## Read State in React

Use the `useAgent()` hook to access agent state:

```tsx
import { useAgent } from "@copilotkit/react-core/v2";

function MyComponent() {
const { agent } = useAgent();
const todos = agent.state?.todos || [];

return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.emoji} {todo.title}</li>
))}
</ul>
);
}
```

## Write State from React

Call `agent.setState()` to update state from the frontend:

```tsx
const toggleTodo = (todoId: string) => {
const updated = todos.map(t =>
t.id === todoId
? { ...t, status: t.status === "completed" ? "pending" : "completed" }
: t
);
agent.setState({ todos: updated });
};

const addTodo = () => {
const newTodo = {
id: crypto.randomUUID(),
title: "New task",
description: "",
emoji: "📝",
status: "pending",
};
agent.setState({ todos: [...todos, newTodo] });
};
```

## Write State from Agent Tools

Tools update state by returning a `Command` with an `update` dict:

```python
# apps/agent/src/todos.py

from langgraph.types import Command
from langchain.tools import tool, ToolRuntime
from langchain.messages import ToolMessage

@tool
def manage_todos(todos: list[Todo], runtime: ToolRuntime) -> Command:
"""Manage the current todos."""
for todo in todos:
if "id" not in todo or not todo["id"]:
todo["id"] = str(uuid.uuid4())

return Command(update={
"todos": todos,
"messages": [
ToolMessage(
content="Successfully updated todos",
tool_call_id=runtime.tool_call_id
)
]
})
```

## Read State in Agent Tools

Use `runtime.state` to read current state:

```python
@tool
def get_todos(runtime: ToolRuntime):
"""Get the current todos."""
return runtime.state.get("todos", [])
```

## How State Flows

1. **User edits a todo** → `agent.setState({ todos: [...] })`
2. **CopilotKit syncs** the change to the agent backend
3. **Agent observes** the updated state via `runtime.state`
4. **Agent calls a tool** → `Command(update={ "todos": [...] })`
5. **CopilotKit syncs** the update back to the frontend
6. **React re-renders** because `agent.state.todos` changed

The key insight: there is no separate frontend state management. State lives in the agent, and CopilotKit handles the sync.

## Adding New State Fields

To add a new field to agent state:

1. Add the field to `AgentState` in Python:
```python
class AgentState(BaseAgentState):
todos: list[Todo]
tags: list[str] # new field
```

2. Read it in React:
```tsx
const tags = agent.state?.tags || [];
```

3. Write it from React or tools the same way as above.
141 changes: 141 additions & 0 deletions docs/agent-tools.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# Agent Tools

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.

## Creating a Tool

Use the `@tool` decorator from LangChain:

```python
from langchain.tools import tool, ToolRuntime

@tool
def my_tool(arg1: str, arg2: int, runtime: ToolRuntime):
"""Description of what this tool does. The agent reads this to decide when to call it."""
return f"Result: {arg1} x {arg2}"
```

- The docstring tells the agent when and how to use the tool
- `runtime: ToolRuntime` gives access to agent state (optional parameter)
- Return value is sent back to the agent as the tool result

## Reading State

Access current agent state via `runtime.state`:

```python
# apps/agent/src/todos.py

@tool
def get_todos(runtime: ToolRuntime):
"""Get the current todos."""
return runtime.state.get("todos", [])
```

## Updating State

Return a `Command` with an `update` dict to modify agent state:

```python
from langgraph.types import Command
from langchain.messages import ToolMessage

@tool
def manage_todos(todos: list[Todo], runtime: ToolRuntime) -> Command:
"""Manage the current todos."""
# Ensure all todos have unique IDs
for todo in todos:
if "id" not in todo or not todo["id"]:
todo["id"] = str(uuid.uuid4())

return Command(update={
"todos": todos,
"messages": [
ToolMessage(
content="Successfully updated todos",
tool_call_id=runtime.tool_call_id
)
]
})
```

Key points:
- `Command(update={...})` merges the update into agent state
- Include a `ToolMessage` in the `messages` list to acknowledge the tool call
- Use `runtime.tool_call_id` for the message's `tool_call_id`

## Returning Data (No State Update)

For tools that just return data without modifying state, return the value directly:

```python
# apps/agent/src/query.py

import csv
from pathlib import Path
from langchain.tools import tool

# Load data at module init
_data = []
with open(Path(__file__).parent / "db.csv") as f:
_data = list(csv.DictReader(f))

@tool
def query_data(query: str):
"""Query the financial transactions database. Call this before creating charts."""
return _data
```

## Registering Tools with the Agent

Add tools to the agent's `tools` list in `apps/agent/main.py`:

```python
from src.todos import todo_tools # [manage_todos, get_todos]
from src.query import query_data
from src.plan import plan_visualization

agent = create_deep_agent(
model=ChatOpenAI(model="gpt-5.4-2026-03-05"),
tools=[query_data, plan_visualization, *todo_tools],
middleware=[CopilotKitMiddleware()],
context_schema=AgentState,
...
)
```

You can pass individual tools or spread a list of tools.

## Example: Adding a New Tool

Say you want to add a tool that fetches weather data:

**1. Create the tool** (`apps/agent/src/weather.py`):

```python
from langchain.tools import tool

@tool
def get_weather(city: str):
"""Get the current weather for a city."""
# Your implementation here
return {"city": city, "temp": 72, "condition": "sunny"}
```

**2. Register it** in `apps/agent/main.py`:

```python
from src.weather import get_weather

agent = create_deep_agent(
tools=[query_data, plan_visualization, *todo_tools, get_weather],
...
)
```

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)).

## Next Steps

- [Agent State](agent-state.md) — How state sync works between tools and the frontend
- [Generative UI](generative-ui.md) — Render custom UI for tool results
Loading
Loading