Skip to content

Latest commit

 

History

History
420 lines (357 loc) · 15.6 KB

File metadata and controls

420 lines (357 loc) · 15.6 KB
title State Rendering
icon lucide/Bot
description Render the state of your agent with custom UI components.

{/* TODO: Swap these links for Microsoft Agent Framework links once Microsoft Agent Framework is officially added to the feature viewer */}

What is this?

Microsoft Agent Framework agents can maintain state throughout their execution. CopilotKit allows you to render this state in your application with custom UI components, which we call Agentic Generative UI. State updates can be streamed to the frontend as your agent processes requests.

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

### Define your agent state
Define a state snapshot class that represents the data you want to stream to the frontend. This class should be JSON-serializable and contain only UI-relevant properties.

<Tabs groupId="language_microsoft-agent-framework_agent" items={['.NET', 'Python']} persist>
  <Tab value=".NET">
    ```csharp title="Program.cs"
    using System.Text.Json.Serialization;

    public class SearchInfo
    {
        [JsonPropertyName("query")]
        public string Query { get; set; } = string.Empty;

        [JsonPropertyName("done")]
        public bool Done { get; set; }
    }

    public class AgentStateSnapshot
    {
        [JsonPropertyName("searches")]
        public List<SearchInfo> Searches { get; set; } = new();
    }
    ```
  </Tab>
  <Tab value="Python">
    ```python title="agent/src/agent.py (excerpt)"
    from typing import Annotated
    from pydantic import BaseModel, Field

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

    # JSON schema used by AG-UI to validate and forward state to the frontend
    STATE_SCHEMA: dict[str, object] = {
        "searches": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "query": {"type": "string"},
                    "done": {"type": "boolean"},
                },
                "required": ["query", "done"],
                "additionalProperties": False,
            },
            "description": "List of searches and whether each is done.",
        }
    }
    ```
  </Tab>
</Tabs>

On the frontend, define the matching TypeScript type:

```tsx title="app/page.tsx"
type SearchInfo = {
  query: string;
  done: boolean;
};

type AgentState = {
  searches: SearchInfo[];
};
```
### Stream state from your agent
To stream state updates to the frontend, wrap your agent with a `DelegatingAIAgent` that intercepts the streaming response and emits state snapshots as `DataContent`.

Here's an example of a state-streaming agent wrapper:

<Tabs groupId="language_microsoft-agent-framework_agent" items={['.NET', 'Python']} persist>
  <Tab value=".NET">
    ```csharp title="Program.cs"
    using System.Runtime.CompilerServices;
    using System.Text.Json;
    using Azure.AI.OpenAI;
    using Azure.Identity;
    using Microsoft.Agents.AI;
    using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Http.Json;
    using Microsoft.Extensions.AI;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Options;

    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddAGUI();
    var app = builder.Build();

    string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"]!;
    string deployment = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"]!;

    // Get JSON serializer options
    var jsonOptions = app.Services.GetRequiredService<IOptions<JsonOptions>>();

    // Create the base agent
    AIAgent baseAgent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
        .GetChatClient(deployment)
        .CreateAIAgent(
            name: "ResearchAssistant",
            instructions: "You are a research assistant that tracks your progress.");

    // Wrap with state-streaming agent
    AIAgent agent = new StateStreamingAgent(baseAgent, jsonOptions.Value.SerializerOptions);

    // Map the AG-UI endpoint
    app.MapAGUI("/", agent);
    await app.RunAsync();

    // Agent wrapper that streams state updates
    internal sealed class StateStreamingAgent : DelegatingAIAgent
    {
        private readonly JsonSerializerOptions _jsonSerializerOptions;

        public StateStreamingAgent(AIAgent innerAgent, JsonSerializerOptions jsonSerializerOptions)
            : base(innerAgent)
        {
            this._jsonSerializerOptions = jsonSerializerOptions;
        }

        public override async IAsyncEnumerable<AgentRunResponseUpdate> RunStreamingAsync(
            IEnumerable<ChatMessage> messages,
            AgentThread? thread = null,
            AgentRunOptions? options = null,
            [EnumeratorCancellation] CancellationToken cancellationToken = default)
        {
            // Get current state from options if provided
            JsonElement currentState = default;
            if (options is ChatClientAgentRunOptions { ChatOptions.AdditionalProperties: { } properties } &&
                properties.TryGetValue("ag_ui_state", out object? stateObj) && stateObj is JsonElement state)
            {
                currentState = state;
            }

            // Create options with JSON schema for structured state output
            ChatClientAgentRunOptions stateOptions = new ChatClientAgentRunOptions
            {
                ChatOptions = new ChatOptions
                {
                    ResponseFormat = ChatResponseFormat.ForJsonSchema<AgentStateSnapshot>(
                        schemaName: "AgentStateSnapshot",
                        schemaDescription: "Research progress state")
                }
            };

            // Add system message with current state
            var stateMessage = new ChatMessage(ChatRole.System,
                $"Current state: {(currentState.ValueKind != JsonValueKind.Undefined ? currentState.GetRawText() : "{}")}");
            var messagesWithState = messages.Append(stateMessage);

            // Collect all updates
            var allUpdates = new List<AgentRunResponseUpdate>();
            await foreach (var update in this.InnerAgent.RunStreamingAsync(messagesWithState, thread, stateOptions, cancellationToken))
            {
                allUpdates.Add(update);
                // Stream non-text updates immediately
                if (update.Contents.Any(c => c is not TextContent))
                {
                    yield return update;
                }
            }

            // Deserialize state snapshot from response
            var response = allUpdates.ToAgentRunResponse();
            if (response.TryDeserialize(this._jsonSerializerOptions, out JsonElement stateSnapshot))
            {
                byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes(
                    stateSnapshot,
                    this._jsonSerializerOptions.GetTypeInfo(typeof(JsonElement)));

                // Emit state snapshot as DataContent
                yield return new AgentRunResponseUpdate
                {
                    Contents = [new DataContent(stateBytes, "application/json")]
                };
            }

            // Stream text summary
            var summaryMessage = new ChatMessage(ChatRole.System, "Provide a brief summary of your progress.");
            await foreach (var update in this.InnerAgent.RunStreamingAsync(
                messages.Concat(response.Messages).Append(summaryMessage), thread, options, cancellationToken))
            {
                yield return update;
            }
        }
    }
    ```
  </Tab>
  <Tab value="Python">
    ```python title="main.py"
    from __future__ import annotations
    import os
    import uvicorn
    from agent_framework import Agent, tool, SupportsChatGetResponse
    from agent_framework.openai import OpenAIChatClient
    from agent_framework.ag_ui import add_agent_framework_fastapi_endpoint
    from agent_framework.ag_ui import AgentFrameworkAgent
    from dotenv import load_dotenv
    from fastapi import FastAPI
    from typing import Annotated
    from pydantic import BaseModel, Field

    load_dotenv()

    class SearchItem(BaseModel):
        query: str
        done: bool
        
    STATE_SCHEMA: dict[str, object] = {
        "searches": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "query": {"type": "string"},
                    "done": {"type": "boolean"},
                },
                "required": ["query", "done"],
                "additionalProperties": False,
            },
            "description": "List of searches and whether each is done.",
        }
    }
    PREDICT_STATE_CONFIG: dict[str, dict[str, str]] = {
        "searches": {
            "tool": "update_searches",
            "tool_argument": "searches",
        }
    }

    @tool
    def update_searches(
        searches: Annotated[list[SearchItem], Field(description=("The complete source of truth for the user's searches. Maintain ordering and include the full list on each call."))],
    ) -> str:
        return f"Searches updated. Tracking {len(searches)} item(s)."


    def _build_chat_client():
        if os.getenv("AZURE_OPENAI_ENDPOINT"):
            return OpenAIChatClient(
                model=os.getenv("AZURE_OPENAI_CHAT_DEPLOYMENT_NAME", "gpt-4o-mini"),
                api_key=os.getenv("AZURE_OPENAI_API_KEY"),
                azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
            )
        if os.getenv("OPENAI_API_KEY"):
            return OpenAIChatClient(
                model=os.getenv("OPENAI_CHAT_MODEL_ID", "gpt-4o-mini"),
                api_key=os.getenv("OPENAI_API_KEY"),
            )
        raise RuntimeError(
            "Set either AZURE_OPENAI_ENDPOINT + AZURE_OPENAI_API_KEY, or OPENAI_API_KEY."
        )



    def create_agent(chat_client: SupportsChatGetResponse) -> AgentFrameworkAgent:
        base_agent = Agent(
            name="search_agent",
            instructions=(
                "You help users create and run searches.\\n\\n"
                "State sync rules:\\n"
                "- Maintain a list of searches: each item has { query, done }.\\n"
                "- When adding a new search, call `update_searches` with the FULL list, including the new item with done=true.\\n"
                "- All searches in the list should have done=true unless explicitly in progress.\\n"
                "- Never send partial updates—always include the full list on each call.\\n"
            ),
            client=chat_client,
            tools=[update_searches],
        )

        return AgentFrameworkAgent(
            agent=base_agent,
            name="CopilotKitMicrosoftAgentFrameworkAgent",
            description="Maintains a list of searches and streams state to the UI.",
            state_schema=STATE_SCHEMA,
            predict_state_config=PREDICT_STATE_CONFIG,
            require_confirmation=False,
        )
    chat_client = _build_chat_client()

    agent = create_agent(chat_client)

    app = FastAPI(title="Microsoft Agent Framework - Quickstart")
    add_agent_framework_fastapi_endpoint(app=app, agent=agent, path="/")

    if __name__ == "__main__":
        uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
    ```
  </Tab>
</Tabs>

<Callout type="info">
  The `DelegatingAIAgent` wrapper intercepts streaming responses, uses JSON schema response format to generate structured state, and emits it as `DataContent` which the AG-UI protocol forwards to the frontend.
</Callout>

<Callout type="tip">
  For a complete, production-ready implementation of state streaming, see the [SharedStateAgent sample](https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/AGUIClientServer/AGUIDojoServer/SharedStateAgent.cs) in the Agent Framework repository.
</Callout>
### 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"
import { useAgent } from "@copilotkit/react-core/v2";

// For type safety, define the state type matching your agent's state snapshot
type AgentState = {
  searches: {
    query: string;
    done: boolean;
  }[];
};

function YourMainContent() {
  // ...

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

  // ...

  return <div>...</div>;
}
```
### 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"
import { useAgent } from "@copilotkit/react-core/v2"; // [!code highlight]
// ...

// Define the state type matching your agent's state snapshot
type AgentState = {
  searches: {
    query: string;
    done: boolean;
  }[];
};

function YourMainContent() {
  // ...

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

  // ...

  return (
    <div>
      {/* ... */}
      <div className="flex flex-col gap-2 mt-4">
        {/* [!code highlight:5] */}
        {agent.state?.searches?.map((search, index) => (
          <div key={index} className="flex flex-row">
            {search.done ? "✅" : "❌"} {search.query}
          </div>
        ))}
      </div>
    </div>
  )
}
```
### Give it a try!
You've now created a component that will render the agent's state in the chat.