Back to Copilotkit

State Rendering

showcase/shell-docs/src/content/docs/integrations/pydantic-ai/generative-ui/state-rendering.mdx

1.57.06.2 KB
Original Source

<IframeSwitcher id="agent-state-example" exampleUrl="https://feature-viewer.copilotkit.ai/langgraph/feature/agentic_generative_ui?sidebar=false&chatDefaultOpen=false" codeUrl="https://feature-viewer.copilotkit.ai/langgraph/feature/agentic_generative_ui?view=code&sidebar=false&codeLayout=tabs" exampleLabel="Demo" codeLabel="Code" height="700px" />

What is this?

All Pydantic AI Agents are stateful. This means that as your agent progresses through nodes, a state object is passed between them perserving the overall state of a session. CopilotKit allows you to render this state in your application with custom UI components, which we call Agentic Generative UI.

When should I use this?

Rendering the state of your agent in the UI is useful when you want to provide the user with feedback about the overall state of a session. A great example of this is a situation where a user and an agent are working together to solve a problem. The agent can store a draft in its state which is then rendered in the UI.

Implementation

<Steps> <Step> ### Run and connect your agent <RunAndConnect /> </Step> <Step> ### Set up your agent with state
Create your Pydantic AI agent with a stateful structure. Here's a complete example that tracks searches:

```python title="agent.py"
from textwrap import dedent
from pydantic import BaseModel, Field
from pydantic_ai import Agent, RunContext
from pydantic_ai.ag_ui import StateDeps
from ag_ui.core import StateSnapshotEvent, EventType


class Search(BaseModel):
    query: str
    done: bool


class AgentState(BaseModel):
    searches: list[Search] = Field(default_factory=list)


agent = Agent("openai:gpt-5.4-mini", deps_type=StateDeps[AgentState])


@agent.tool
async def add_search(
    ctx: RunContext[StateDeps[AgentState]], new_query: str
) -> StateSnapshotEvent:
    """Add a search to the agent's list of searches."""
    new_search = Search(query=new_query, done=False)
    searches = ctx.deps.state.searches
    searches.append(new_search)
    agent_state = AgentState(searches=searches)
    ctx.deps.state = agent_state

    return StateSnapshotEvent(type=EventType.STATE_SNAPSHOT, snapshot=agent_state)


@agent.tool
async def run_searches(ctx: RunContext[StateDeps[AgentState]]) -> StateSnapshotEvent:
    """Run the searches in the agent's state."""
    searches = ctx.deps.state.searches

    for search in searches:
        await asyncio.sleep(1)
        search.done = True

    agent_state = AgentState(searches=searches)
    ctx.deps.state = agent_state

    return StateSnapshotEvent(type=EventType.STATE_SNAPSHOT, snapshot=agent_state)


@agent.instructions()
async def search_instructions(ctx: RunContext[StateDeps[AgentState]]) -> str:
    """Instructions for the search agent."""
    return dedent(
        f"""
        You are a helpful assistant for storing searches.

        IMPORTANT:
        - Use the `add_search` tool to add a search to the agent's state
        - After using the `add_search` tool, YOU MUST ALWAYS use the `run_searches` tool to run the searches
        - ONLY USE THE `add_search` TOOL ONCE FOR A GIVEN QUERY

        Current searches:
        {ctx.deps.state.model_dump_json(indent=2)}
        """
    )


app = agent.to_ag_ui(deps=StateDeps(AgentState()))

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)
```
</Step> <Step> ### Render state of the agent in the chat Now we can utilize `useAgent` with a `render` function to render the state of our agent **in the chat**.
```tsx title="app/page.tsx"
// ...
// ...

// Define the state of the agent, should match the state of your Pydantic AI Agent.
type AgentState = {
  searches: {
    query: string;
    done: boolean;
  }[];
};

function YourMainContent() {
  // ...

  // [!code highlight:13]
  // styles omitted for brevity
  useAgent({
    agentId: "my_agent",
    render: ({ state }) => (
      <div>
        {state.searches?.map((search, index) => (
          <div key={index}>
            {search.done ? "✅" : "❌"} {search.query}{search.done ? "" : "..."}
          </div>
        ))}
      </div>
    ),
  });

  // ...

  return <div>...</div>;
}
```

<Callout type="warn" title="Important">
  The `name` parameter must exactly match the agent name you defined in your CopilotRuntime configuration (e.g., `my_agent` from the quickstart).
</Callout>
</Step> <Step> ### Render state outside of the chat You can also render the state of your agent **outside of the chat**. This is useful when you want to render the state of your agent anywhere other than the chat.
```tsx title="app/page.tsx"
// ...

// Define the state of the agent, should match the state of your Pydantic AI Agent.
type AgentState = {
  searches: {
    query: string;
    done: boolean;
  }[];
};

function YourMainContent() {
  // ...

  // [!code highlight:3]
  const { agent } = useAgent({
    agentId: "my_agent",
  })

  // ...

  return (
    <div>
      <div className="flex flex-col gap-2 mt-4">
        {agent.state?.searches?.map((search, index) => (
          <div key={index} className="flex flex-row">
            {search.done ? "✅" : "❌"} {search.query}
          </div>
        ))}
      </div>
    </div>
  )
}
```

<Callout type="warn" title="Important">
  The `name` parameter must exactly match the agent name you defined in your CopilotRuntime configuration (e.g., `my_agent` from the quickstart).
</Callout>
</Step> <Step> ### Give it a try!
You've now created a component that will render the agent's state in the chat.

<video
  src="https://cdn.copilotkit.ai/docs/copilotkit/images/coagents/agentic-generative-ui.mp4"
  className="rounded-lg shadow-xl"
  loop
  playsInline
  controls
  autoPlay
  muted
/>
</Step> </Steps>