Zustandsverwaltung mit AG-UI

In diesem Lernprogramm erfahren Sie, wie Sie die Zustandsverwaltung mit AG-UI implementieren und die bidirektionale Synchronisierung des Zustands zwischen Client und Server aktivieren. Dies ist für die Erstellung interaktiver Anwendungen wie generative UI, Echtzeitdashboards oder gemeinsame Erfahrungen unerlässlich.

Voraussetzungen

Bevor Sie beginnen, stellen Sie sicher, dass Sie Folgendes verstehen:

Was ist Zustandsverwaltung?

Die Statusverwaltung in AG-UI ermöglicht Folgendes:

  • Freigegebener Zustand: Sowohl Client als auch Server verwalten eine synchronisierte Ansicht des Anwendungszustands
  • Bidirektionale Synchronisierung: Status kann vom Client oder Server aktualisiert werden.
  • Echtzeitaktualisierungen: Änderungen werden sofort mithilfe von Zustandsereignissen gestreamt.
  • Predictive Updates: Statusaktualisierungsstream, wenn die LLM Toolargumente generiert (optimistische Benutzeroberfläche)
  • Strukturierte Daten: State folgt einem JSON-Schema für die Validierung.

Anwendungsfälle

Die Zustandsverwaltung ist nützlich für:

  • Generative UI: Erstellen von UI-Komponenten basierend auf dem vom Agent gesteuerten Zustand
  • Formularerstellung: Der Agent füllt Formularfelder auf, während er Informationen sammelt.
  • Fortschrittsverfolgung: Anzeigen des Echtzeitfortschritts von mehrstufigen Vorgängen
  • Interaktive Dashboards: Anzeigen von Daten, die aktualisiert werden, während der Agent sie verarbeitet
  • Gemeinsame Bearbeitung: Mehrere Benutzer sehen konsistente Statusaktualisierungen

Erstellen von zustandsbewussten Agenten in C#

Die Zustandsverwaltung in der .NET AG-UI Integration ist deklarativ: Ihr Agent macht gewöhnliche Tools verfügbar, die Ihre Zustandsobjekte zurückgeben, und Sie teilen der Hostebene mit, welche Toolergebnisse AG-UI Zustandsereignisse werden, indem Sie eine Konfiguration konfigurierenAGUIStreamOptions. Sie schreiben keinen benutzerdefinierten Agent oder geben Protokollinhalte manuell aus.

Definieren des Statusmodells

Definieren Sie zunächst Klassen für ihre Zustandsstruktur:

using System.Text.Json.Serialization;

namespace RecipeAssistant;

// State response wrapper returned by the tool. Its shape is what the client renders as state.
internal sealed class RecipeResponse
{
    [JsonPropertyName("recipe")]
    public Recipe Recipe { get; set; } = new();
}

// Recipe state model.
internal sealed class Recipe
{
    [JsonPropertyName("title")]
    public string Title { get; set; } = string.Empty;

    [JsonPropertyName("skill_level")]
    public string SkillLevel { get; set; } = string.Empty;

    [JsonPropertyName("cooking_time")]
    public string CookingTime { get; set; } = string.Empty;

    [JsonPropertyName("special_preferences")]
    public List<string> SpecialPreferences { get; set; } = [];

    [JsonPropertyName("ingredients")]
    public List<Ingredient> Ingredients { get; set; } = [];

    [JsonPropertyName("instructions")]
    public List<string> Instructions { get; set; } = [];
}

// A single ingredient.
internal sealed class Ingredient
{
    [JsonPropertyName("icon")]
    public string Icon { get; set; } = string.Empty;

    [JsonPropertyName("name")]
    public string Name { get; set; } = string.Empty;

    [JsonPropertyName("amount")]
    public string Amount { get; set; } = string.Empty;
}

// JSON serialization context for the tool payloads.
[JsonSerializable(typeof(RecipeResponse))]
[JsonSerializable(typeof(Recipe))]
[JsonSerializable(typeof(Ingredient))]
internal sealed partial class RecipeSerializerContext : JsonSerializerContext;

Ausgeben einer Zustandsmomentaufnahme aus einem Tool

Machen Sie ein Tool verfügbar, das den vollständigen Zustand zurückgibt. Der Agent ruft ihn immer dann auf, wenn sich das Rezept ändern sollte. Die Hostingebene wandelt das Toolergebnis in ein STATE_SNAPSHOT Ereignis um, wenn Sie es zuordnen:

using System.ComponentModel;
using Microsoft.Extensions.AI;

[Description("Generate or update the shared recipe and display it to the user.")]
static RecipeResponse GenerateRecipe(
    [Description("The complete recipe to display.")] Recipe recipe) => new() { Recipe = recipe };

AITool generateRecipe = AIFunctionFactory.Create(
    GenerateRecipe,
    name: "generate_recipe",
    description: "Generate or update the shared recipe and display it to the user.",
    RecipeSerializerContext.Default.Options);

Erstellen des Agents

Erstellen Sie den Agent direkt von Ihrem Chatclient mit ChatClientAgentOptions. Setzen Sie die Systemaufforderung und -tools auf ChatOptions:

using Microsoft.Agents.AI;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Extensions.AI;
using OpenAI.Chat;

const string SharedStateSystemPrompt =
    """
    You are a helpful recipe assistant that maintains a shared recipe state with the user.

    IMPORTANT:
    - When the user asks you to create, change, or improve a recipe, call the `generate_recipe`
      tool with a COMPLETE recipe: a title, skill_level, cooking_time, special_preferences, the
      full list of ingredients (each with an icon, name and amount) and the step-by-step
      instructions.
    - Always include every ingredient the recipe needs, keeping any the user already added.
    - When the user only asks a question about the recipe, answer in plain text and do NOT call the tool.
    """;

string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
    ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME")
    ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");

AIAgent recipeAgent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
    .GetChatClient(deploymentName)
    .AsAIAgent(new ChatClientAgentOptions
    {
        Name = "RecipeAgent",
        Description = "An agent that maintains a shared recipe state with the user.",
        ChatOptions = new ChatOptions
        {
            Instructions = SharedStateSystemPrompt,
            Tools = [generateRecipe],
        },
    });

Warning

DefaultAzureCredential ist praktisch für die Entwicklung, erfordert aber sorgfältige Überlegungen in der Produktion. Berücksichtigen Sie in der Produktion die Verwendung bestimmter Anmeldeinformationen (z. B. ManagedIdentityCredential), um Latenzprobleme, unbeabsichtigte Abfragen von Anmeldeinformationen und potenzielle Sicherheitsrisiken durch Ausweichmechanismen zu vermeiden.

Zuordnen des Toolergebnisses zu einem Statusereignis

Erstellen Sie einen AGUIStreamOptions, registrieren Sie den Toolnamen als Zustandsmomentaufnahme, und fügen Sie ihn an die Endpunktmetadaten an. MapAGUIServer liest die Datenstromoptionen vom Endpunkt (oder aus IOptions<AGUIStreamOptions> di) und gibt die Statusereignisse für Sie aus:

using AGUI.Server;
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.ConfigureHttpJsonOptions(options =>
    options.SerializerOptions.TypeInfoResolverChain.Add(RecipeSerializerContext.Default));
builder.Services.AddAGUIServer();

// A `generate_recipe` result becomes a STATE_SNAPSHOT event.
AGUIStreamOptions streamOptions = new AGUIStreamOptions()
    .MapResultAsStateSnapshot("generate_recipe");

WebApplication app = builder.Build();

// Attach the stream options to the endpoint. MapAGUIServer emits the state events for you.
app.MapAGUIServer("/", recipeAgent).WithMetadata(streamOptions);

await app.RunAsync();

Das ist der gesamte Server. Es gibt keinen benutzerdefinierten DelegatingAIAgent und keinen zu erstellenden Protokollinhalt: Das Tool gibt Ihr Statusobjekt zurück, MapResultAsStateSnapshot wandelt jedes Ergebnis in ein STATE_SNAPSHOT, und das Framework streamt es an den Client.

Leseclientstatus

Das Rezept lebt auf dem Kunden. Wenn der Client eine Drehung sendet, enthält er seinen aktuellen Zustand auf dem AG-UI RunAgentInput. Wiederherstellen der Anforderung ChatOptions mit und Lesen RunAgentInput.State (aJsonElementTryGetRunAgentInput):

using System.Text.Json;
using AGUI.Abstractions;
using AGUI.Server;
using Microsoft.Extensions.AI;

static bool TryGetClientState(ChatOptions chatOptions, out JsonElement state)
{
    if (chatOptions.TryGetRunAgentInput(out RunAgentInput? input) &&
        input.State is { ValueKind: not JsonValueKind.Undefined } clientState)
    {
        state = clientState;
        return true;
    }

    state = default;
    return false;
}

TryGetRunAgentInput liest die Eingabe, auf der die Hostebene gestaffelt ist ChatOptions.AdditionalProperties. Sie berühren dieses Wörterbuch nie direkt. Weisen Sie dem Modell das aktuelle Rezept zu, indem Sie es als Systemmeldung vor der Ausführung des Agents ausstehen (z. B. aus einem einfachen DelegatingAIAgent Element, das nur Kontext eingibt und die Ausführung delegiert), sodass Bearbeitungen auf dem vorhandenen Zustand aufbauen, anstatt von Grund auf neu zu beginnen.

Wichtige Konzepte

  • Rückgabestatus der Tools: Ein Tool gibt ihr Statusobjekt zurück; Sie erstellen nie AG-UI Ereignisse selbst.
  • Deklarative Zuordnung: AGUIStreamOptions.MapResultAsStateSnapshot(toolName) / MapResultAsStateDelta(toolName) Zuordnen eines Toolergebnisses zu einem STATE_SNAPSHOT / STATE_DELTA Ereignis.
  • Endpunktverknüpfung: Fügen Sie die Datenstromoptionen an .WithMetadata(streamOptions) , MapAGUIServeroder registrieren Sie sie IOptions<AGUIStreamOptions> in DI.
  • Lesezustand: ChatOptions.TryGetRunAgentInput(out var input) Stellt den RunAgentInput; input.State ist der aktuelle Zustand des Clients als ein JsonElement.

Statusdelta mit Agentic Generative UI

MapResultAsStateSnapshot Ersetzt den gesamten Zustand bei jeder Aktivierung. Ordnen Sie für inkrementelle Änderungen ein Toolergebnis einem STATE_DELTA Mit MapResultAsStateDelta- und Rückgabe eines JSON-Patchdokuments zu.

Ein gängiges Szenario ist eine agentische generative UI: Der Agent erstellt einen Plan und aktualisiert dann den Status einzelner Schritte während der Funktionsweise. create_plan sendet den vollständigen Plan als Momentaufnahme; update_plan_step sendet nur die geänderten Felder als Delta.

Definieren Sie das Planmodell und die Statusenume:

using System.Text.Json.Serialization;

internal sealed class Plan
{
    [JsonPropertyName("steps")]
    public List<Step> Steps { get; set; } = [];
}

internal sealed class Step
{
    [JsonPropertyName("description")]
    public required string Description { get; set; }

    [JsonPropertyName("status")]
    public StepStatus Status { get; set; } = StepStatus.Pending;
}

[JsonConverter(typeof(JsonStringEnumConverter<StepStatus>))]
internal enum StepStatus
{
    Pending,
    Completed
}

internal sealed class JsonPatchOperation
{
    [JsonPropertyName("op")]
    public required string Op { get; set; }

    [JsonPropertyName("path")]
    public required string Path { get; set; }

    [JsonPropertyName("value")]
    public object? Value { get; set; }
}

Das create_plan Tool gibt den vollständigen Plan zurück; update_plan_step gibt eine Liste der JSON-Patchvorgänge zurück:

using System.ComponentModel;

[Description("Create a plan with multiple steps.")]
public static Plan CreatePlan(
    [Description("List of step descriptions to create the plan.")] List<string> steps)
{
    return new Plan
    {
        Steps = [.. steps.Select(s => new Step { Description = s, Status = StepStatus.Pending })]
    };
}

[Description("Update a step in the plan with new description or status.")]
public static List<JsonPatchOperation> UpdatePlanStep(
    [Description("The index of the step to update.")] int index,
    [Description("The new status for the step.")] StepStatus status)
{
    // Status must be lowercase to match AG-UI frontend expectations.
    string statusValue = status == StepStatus.Pending ? "pending" : "completed";

    return
    [
        new JsonPatchOperation { Op = "replace", Path = $"/steps/{index}/status", Value = statusValue }
    ];
}

Registrieren Sie beide Tools im Agent (damit AllowMultipleToolCalls = false aktualisiert das Modell jeweils einen Schritt), und ordnen Sie jedes Toolergebnis dem übereinstimmenden Zustandsereignis zu: create_plan einer Momentaufnahme, update_plan_step einem Delta.

using AGUI.Server;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;

AITool createPlan = AIFunctionFactory.Create(
    CreatePlan, name: "create_plan", description: "Create a plan with multiple steps.");
AITool updatePlanStep = AIFunctionFactory.Create(
    UpdatePlanStep, name: "update_plan_step", description: "Update a step in the plan with new description or status.");

AIAgent planAgent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
    Name = "AgenticUIAgent",
    ChatOptions = new ChatOptions
    {
        Instructions = "Use `create_plan` to set the initial steps, then call `update_plan_step` until every step is completed. Do not describe the plan in text.",
        Tools = [createPlan, updatePlanStep],
        AllowMultipleToolCalls = false,
    },
});

AGUIStreamOptions planStreamOptions = new AGUIStreamOptions()
    .MapResultAsStateSnapshot("create_plan")   // full plan -> STATE_SNAPSHOT
    .MapResultAsStateDelta("update_plan_step"); // JSON Patch -> STATE_DELTA

app.MapAGUIServer("/agentic_generative_ui", planAgent).WithMetadata(planStreamOptions);

Note

STATE_SNAPSHOT ersetzt den gesamten Zustand; STATE_DELTA wendet einen JSON-Patch auf den vorhandenen Zustand an. Senden Sie eine Momentaufnahme, wenn Sie den Zustand einrichten oder zurücksetzen, und Deltas für inkrementelle Änderungen.

Für ein verwandtes Muster, das die Argumente eines Tools in den Zustand überträgt, während das Modell sie generiert, fahren Sie mit den nachfolgenden Predictive State Updates fort.

Vorhersagende Statusaktualisierungen

Durch Vorhersagezustandsaktualisierungen kann die Benutzeroberfläche auf einen Toolaufruf reagieren, während ihre Argumente noch generiert werden, anstatt darauf zu warten, dass das Tool abgeschlossen ist. Während das Modell die Argumente für ein Tool streamt, wandelt der Server diese Partielle Argumente in Zustandsmomentaufnahmen um und sendet sie an den Client. Der Client rendert jede Momentaufnahme sofort, sodass der Benutzer eine optimistische Livevorschau erhält. Beispielsweise zeigt ein Dokument-Editor den Text in Echtzeit an und fordert den Benutzer dann auf, die Änderung nach Abschluss des Modells zu bestätigen.

Note

In diesem Szenario wird der Endpunkt manuell zugeordnet und der integrierte TypedResults.ServerSentEvents(...)Endpunkt verwendet, der .NET 10.0 oder höher erfordert.

Funktionsweise

Im Gegensatz zum Szenario mit freigegebenem Zustand , bei dem ein Tool ausgeführt wird und das Ergebnis zu einer Momentaufnahme wird, fängt das Vorhersageszenario den Toolaufruf ab, bevor es ausgeführt wird und das Argument in den Zustand überträgt:

  1. Der Agent deklariert ein write_document_local Tool. Das Modell ruft es mit dem vollständigen Dokumenttext als document Argument auf.
  2. Das Tool wird nicht serverseitig ausgeführt. Stattdessen fängt eine AGUIStreamOptionsMapCall Zuordnung den Anruf ab.
  3. Die Zuordnung gibt eine Reihe von STATE_SNAPSHOT Ereignissen aus, die jeweils ein progressives längeres Präfix des Dokuments aufweisen, sodass der Client den Textstream sieht.
  4. Anschließend wird der Toolaufruf mit einem TOOL_CALL_RESULT Ereignis abgeschlossen und ein clientseitiger confirm_changes Toolaufruf eingefügt, damit der Client den Benutzer zur Genehmigung auffordern kann.
  5. Der Client rendert jede Momentaufnahme und zeigt die Bestätigungs-/Ablehnungsaufforderung an.

Da die Zuordnung das Toolergebnis selbst erzeugt, wird das Dokumenttool deklariert, aber nie aufgerufen. Der Chatclient wird ohne Funktionsaufruf erstellt.

Definieren des Statusmodells

Das Zustandsmodell beschreibt die Form, die der Client rendert. Verwenden Sie diese Eigenschaft JsonPropertyName , damit die Eigenschaftennamen den Erwartungen des Clients entsprechen:

using System.Text.Json;
using System.Text.Json.Serialization;

internal sealed class DocumentState
{
    [JsonPropertyName("document")]
    public string Document { get; set; } = string.Empty;
}

[JsonSerializable(typeof(DocumentState))]
[JsonSerializable(typeof(JsonElement))]
internal sealed partial class DocumentSerializerContext : JsonSerializerContext;

Deklarieren des Dokumenttools

Die Toolsignatur ist das, was das Modell ausfüllt. Es wird deklariert, sodass das Modell sie aufruft, aber sein Ergebnis wird von der Datenstromzuordnung erstellt, nicht durch Ausführen des Methodentexts:

using System.ComponentModel;
using Microsoft.Extensions.AI;

[Description("Write a document in markdown format.")]
static string WriteDocument(
    [Description("The document content to write.")] string document) => "Document written successfully";

AITool writeDocument = AIFunctionFactory.Create(
    WriteDocument,
    name: "write_document_local",
    description: "Write a document. Use markdown formatting to format the document.");

Konfigurieren der Predictive Stream Mapping

Registrieren Sie eine MapCall Zuordnung für das Tool. Wenn das Modell aufruft write_document_local, liest die Zuordnung das gestreamte document Argument, gibt progressive StateSnapshotEvent Momentaufnahmen aus, schließt den Toolaufruf ab und fügt einen confirm_changes clientseitigen Toolaufruf ein:

using System.Text.Json;
using AGUI.Abstractions;
using AGUI.Server;
using Microsoft.Extensions.AI;

static AGUIStreamOptions CreatePredictiveStreamOptions(JsonSerializerOptions jsonSerializerOptions)
{
    string? lastEmittedDocument = null;

    return new AGUIStreamOptions().MapCall("write_document_local", fcc =>
    {
        string? document = fcc.Arguments?.TryGetValue("document", out var value) == true
            ? value?.ToString()
            : null;

        if (document is null || document == lastEmittedDocument)
        {
            return [];
        }

        var events = new List<BaseEvent>();

        // Only stream the newly added portion if the document grew.
        int startIndex = lastEmittedDocument is not null &&
            document.StartsWith(lastEmittedDocument, StringComparison.Ordinal)
                ? lastEmittedDocument.Length
                : 0;

        const int chunkSize = 10;
        for (int i = startIndex; i < document.Length; i += chunkSize)
        {
            int length = Math.Min(chunkSize, document.Length - i);
            var snapshot = new DocumentState { Document = document[..(i + length)] };
            JsonElement snapshotJson = JsonSerializer.SerializeToElement(snapshot, jsonSerializerOptions);

            events.Add(new StateSnapshotEvent { Snapshot = snapshotJson });
        }

        // Complete the write_document_local call (its document is now reflected in state) so the
        // only tool call the client sees pending is confirm_changes.
        events.Add(new ToolCallResultEvent
        {
            MessageId = Guid.NewGuid().ToString("N"),
            ToolCallId = fcc.CallId,
            Content = "Document written.",
            Role = "tool",
        });

        // Inject a client-side confirm_changes tool call so the approval modal renders.
        string confirmCallId = Guid.NewGuid().ToString("N");
        string confirmMessageId = Guid.NewGuid().ToString("N");
        events.Add(new ToolCallStartEvent { ToolCallId = confirmCallId, ToolCallName = "confirm_changes", ParentMessageId = confirmMessageId });
        events.Add(new ToolCallArgsEvent { ToolCallId = confirmCallId, Delta = "{}" });
        events.Add(new ToolCallEndEvent { ToolCallId = confirmCallId });

        lastEmittedDocument = document;
        return events;
    });
}

Note

Jede Momentaufnahme enthält das vollständige Dokument bis zu diesem Punkt, sodass der Client immer eine konsistente Ansicht rendert, auch wenn es eine Zwischenaktualisierung verpasst.

Zuordnen des Endpunkts

Da die Zuordnung das Toolergebnis selbst erzeugt, erstellen Sie den Chatclient ohne Funktionsaufruf und Stream über die AG-UI Pipeline direkt: passen Sie den eingehenden RunAgentInput Mit ToChatRequestContext- , Anruf GetStreamingResponseAsyncan und konvertieren Sie die Updates mit AsAGUIEventStreamAsync.

using AGUI.Abstractions;
using AGUI.Server;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Options;
using JsonOptions = Microsoft.AspNetCore.Http.Json.JsonOptions;

const string PredictiveSystemPrompt =
    """
    You are a document editor assistant. When asked to write or edit content:
    - Use the `write_document_local` tool with the full document text in Markdown format.
    - You MUST write the full document, even when changing only a few words.
    - When making edits, keep them minimal. Do not change every word.
    After writing the document, briefly summarize the changes you made in at most two sentences.
    """;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.ConfigureHttpJsonOptions(options =>
    options.SerializerOptions.TypeInfoResolverChain.Add(DocumentSerializerContext.Default));
builder.Services.AddAGUIServer();

string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"]
    ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"]
    ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");

// No UseFunctionInvocation: the call is intercepted by the stream mapping, not executed.
IChatClient chatClient = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
    .GetChatClient(deploymentName)
    .AsIChatClient();

WebApplication app = builder.Build();

JsonSerializerOptions jsonSerializerOptions = app.Services
    .GetRequiredService<IOptions<JsonOptions>>()
    .Value.SerializerOptions;

app.MapPost("/", (
    [FromBody] RunAgentInput input,
    HttpContext httpContext,
    CancellationToken cancellationToken) =>
{
    AGUIStreamOptions streamOptions = CreatePredictiveStreamOptions(jsonSerializerOptions);

    ChatRequestContext ctx = input.ToChatRequestContext(jsonSerializerOptions, streamOptions);
    ctx.Messages.Insert(0, new ChatMessage(ChatRole.System, PredictiveSystemPrompt));
    (ctx.ChatOptions.Tools ??= []).Add(writeDocument);

    var updates = chatClient.GetStreamingResponseAsync(ctx.Messages, ctx.ChatOptions, cancellationToken);
    IAsyncEnumerable<BaseEvent> events = updates.AsAGUIEventStreamAsync(ctx, cancellationToken);

    return TypedResults.ServerSentEvents(events);
});

await app.RunAsync();

Warning

DefaultAzureCredential ist praktisch für die Entwicklung, erfordert aber sorgfältige Überlegungen in der Produktion. Berücksichtigen Sie in der Produktion die Verwendung bestimmter Anmeldeinformationen (z. B. ManagedIdentityCredential), um Latenzprobleme, unbeabsichtigte Abfragen von Anmeldeinformationen und potenzielle Sicherheitsrisiken durch Ausweichmechanismen zu vermeiden.

Note

confirm_changes ist ein clientseitiges Tool. Die Datenstromzuordnung fordert sie an, und der Client rendert die Genehmigungsaufforderung. Siehe "Human-in-the-Loop " für das clientseitige Toolmuster.

Prädiktive Schlüsselkonzepte

  • AGUIStreamOptions.MapCall: Fängt einen Toolaufruf (vor der Ausführung) ab und gibt die AG-UI Ereignisse zurück, die dafür ausgegeben werden sollen.
  • FunctionCallContent.Arguments: Die Argumente des gestreamten Tools. Lesen Sie Arguments["document"] , um den Text abzurufen, während das Modell ihn erzeugt.
  • StateSnapshotEvent: Jede Momentaufnahme enthält bisher das vollständige Dokumentpräfix, was den optimistischen Streamingeffekt erzeugt.
  • ToChatRequestContext / AsAGUIEventStreamAsync: Die AG-UI Streamingpipeline, die eine RunAgentInput an eine Chatanfrage anpasst und die Antwortaktualisierungen wieder in AG-UI Ereignisse konvertiert.
  • confirm_changes: Ein clientseitiger Toolaufruf, der nach dem Schreiben des Dokuments eingefügt wurde, sodass der Benutzer das Ergebnis genehmigen kann.

Rendern auf dem Client

Ein UI-Toolkit wie CopilotKit abonniert die Zustandsmomentaufnahmen und rendert das Dokument erneut auf jedem, und zeigt dann die Bestätigungs- oder Ablehnungsaufforderung an, wenn der confirm_changes Toolaufruf eingeht. Dieses Szenario wird im AG-UI Dojo ausgeführt.

Definieren von Zustandsmodellen

Definieren Sie zuerst Pydantische Modelle für Ihre Zustandsstruktur. Dadurch wird die Typsicherheit und Validierung sichergestellt:

from enum import Enum
from pydantic import BaseModel, Field


class SkillLevel(str, Enum):
    """The skill level required for the recipe."""
    BEGINNER = "Beginner"
    INTERMEDIATE = "Intermediate"
    ADVANCED = "Advanced"


class CookingTime(str, Enum):
    """The cooking time of the recipe."""
    FIVE_MIN = "5 min"
    FIFTEEN_MIN = "15 min"
    THIRTY_MIN = "30 min"
    FORTY_FIVE_MIN = "45 min"
    SIXTY_PLUS_MIN = "60+ min"


class Ingredient(BaseModel):
    """An ingredient with its details."""
    icon: str = Field(..., description="Emoji icon representing the ingredient (e.g., 🥕)")
    name: str = Field(..., description="Name of the ingredient")
    amount: str = Field(..., description="Amount or quantity of the ingredient")


class Recipe(BaseModel):
    """A complete recipe."""
    title: str = Field(..., description="The title of the recipe")
    skill_level: SkillLevel = Field(..., description="The skill level required")
    special_preferences: list[str] = Field(
        default_factory=list, description="Dietary preferences (e.g., Vegetarian, Gluten-free)"
    )
    cooking_time: CookingTime = Field(..., description="The estimated cooking time")
    ingredients: list[Ingredient] = Field(..., description="Complete list of ingredients")
    instructions: list[str] = Field(..., description="Step-by-step cooking instructions")

Statusschema

Definieren Sie ein Statusschema, um die Struktur und die Typen Ihres Zustands anzugeben:

state_schema = {
    "recipe": {"type": "object", "description": "The current recipe"},
}

Note

Das Zustandsschema verwendet ein einfaches Format mit type und optional description. Die tatsächliche Struktur wird durch Ihre pydantischen Modelle definiert.

Vorhersagende Statusaktualisierungen

Prädiktiver Status überträgt Argumente von Stream-Tool auf den Status, während LLM sie generiert, und ermöglicht damit optimistische UI-Aktualisierungen.

predict_state_config = {
    "recipe": {"tool": "update_recipe", "tool_argument": "recipe"},
}

Diese Konfiguration ordnet das recipe Statusfeld dem recipe Argument des update_recipe Tools zu. Wenn der Agent das Tool aufruft, werden die Argumente in Echtzeit in den Zustand übertragen, während die LLM sie generiert.

Definieren des Statusaktualisierungstools

Erstellen Sie eine Toolfunktion, die Ihr Pydantisches Modell akzeptiert:

from agent_framework import tool


@tool
def update_recipe(recipe: Recipe) -> str:
    """Update the recipe with new or modified content.

    You MUST write the complete recipe with ALL fields, even when changing only a few items.
    When modifying an existing recipe, include ALL existing ingredients and instructions plus your changes.
    NEVER delete existing data - only add or modify.

    Args:
        recipe: The complete recipe object with all details

    Returns:
        Confirmation that the recipe was updated
    """
    return "Recipe updated."

Important

Der Parametername der Toolfunktion (recipe) muss mit dem tool_argument in Ihrer predict_state_configFunktion übereinstimmen.

Agent mit Statusverwaltung erstellen

Hier ist eine vollständige Serverimplementierung mit Statusverwaltung:

"""AG-UI server with state management."""

from agent_framework import Agent
from agent_framework.openai import OpenAIChatCompletionClient
from agent_framework_ag_ui import (
    AgentFrameworkAgent,
    add_agent_framework_fastapi_endpoint,
)
from azure.identity import AzureCliCredential
from fastapi import FastAPI

# Create the chat agent with tools
agent = Agent(
    name="recipe_agent",
    instructions="""You are a helpful recipe assistant that creates and modifies recipes.

    CRITICAL RULES:
    1. You will receive the current recipe state in the system context
    2. To update the recipe, you MUST use the update_recipe tool
    3. When modifying a recipe, ALWAYS include ALL existing data plus your changes in the tool call
    4. NEVER delete existing ingredients or instructions - only add or modify
    5. After calling the tool, provide a brief conversational message (1-2 sentences)

    When creating a NEW recipe:
    - Provide all required fields: title, skill_level, cooking_time, ingredients, instructions
    - Use actual emojis for ingredient icons (🥕 🧄 🧅 🍅 🌿 🍗 🥩 🧀)
    - Leave special_preferences empty unless specified
    - Message: "Here's your recipe!" or similar

    When MODIFYING or IMPROVING an existing recipe:
    - Include ALL existing ingredients + any new ones
    - Include ALL existing instructions + any new/modified ones
    - Update other fields as needed
    - Message: Explain what you improved (e.g., "I upgraded the ingredients to premium quality")
    - When asked to "improve", enhance with:
      * Better ingredients (upgrade quality, add complementary flavors)
      * More detailed instructions
      * Professional techniques
      * Adjust skill_level if complexity changes
      * Add relevant special_preferences

    Example improvements:
    - Upgrade "chicken" → "organic free-range chicken breast"
    - Add herbs: basil, oregano, thyme
    - Add aromatics: garlic, shallots
    - Add finishing touches: lemon zest, fresh parsley
    - Make instructions more detailed and professional
    """,
    client=OpenAIChatCompletionClient(
        model=deployment_name,
        azure_endpoint=endpoint,
        api_version=os.getenv("AZURE_OPENAI_API_VERSION"),
        credential=AzureCliCredential(),
    ),
    tools=[update_recipe],
)

# Wrap agent with state management
recipe_agent = AgentFrameworkAgent(
    agent=agent,
    name="RecipeAgent",
    description="Creates and modifies recipes with streaming state updates",
    state_schema={
        "recipe": {"type": "object", "description": "The current recipe"},
    },
    predict_state_config={
        "recipe": {"tool": "update_recipe", "tool_argument": "recipe"},
    },
)

# Create FastAPI app
app = FastAPI(title="AG-UI Recipe Assistant")
add_agent_framework_fastapi_endpoint(app, recipe_agent, "/")

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8888)

Wichtige Konzepte

  • Pydantische Modelle: Definieren eines strukturierten Zustands mit Typsicherheit und Validierung
  • Statusschema: Einfaches Format, das Zustandsfeldtypen angibt
  • Predictive State Config: Ordnet Zustandsfelder den Toolargumenten für Streaming-Updates zu
  • Zustandseinfügung: Der aktuelle Zustand wird automatisch als Systemmeldungen eingefügt, um Kontext bereitzustellen.
  • Vollständige Updates: Tools müssen den vollständigen Zustand schreiben, nicht nur Deltas
  • Bestätigungsstrategie: Anpassen von Genehmigungsmeldungen für Ihre Domäne (Rezept, Dokument, Aufgabenplanung usw.)

Verstehen von Statusereignissen

State Snapshot-Ereignis

Eine vollständige Momentaufnahme des aktuellen Zustands, die beim Abschluss des Tools ausgegeben wird:

{
    "type": "STATE_SNAPSHOT",
    "snapshot": {
        "recipe": {
            "title": "Classic Pasta Carbonara",
            "skill_level": "Intermediate",
            "special_preferences": ["Authentic Italian"],
            "cooking_time": "30 min",
            "ingredients": [
                {"icon": "🍝", "name": "Spaghetti", "amount": "400g"},
                {"icon": "🥓", "name": "Guanciale or bacon", "amount": "200g"},
                {"icon": "🥚", "name": "Egg yolks", "amount": "4"},
                {"icon": "🧀", "name": "Pecorino Romano", "amount": "100g grated"},
                {"icon": "🧂", "name": "Black pepper", "amount": "To taste"}
            ],
            "instructions": [
                "Bring a large pot of salted water to boil",
                "Cut guanciale into small strips and fry until crispy",
                "Beat egg yolks with grated Pecorino and black pepper",
                "Cook spaghetti until al dente",
                "Reserve 1 cup pasta water, then drain pasta",
                "Remove pan from heat, add hot pasta to guanciale",
                "Quickly stir in egg mixture, adding pasta water to create creamy sauce",
                "Serve immediately with extra Pecorino and black pepper"
            ]
        }
    }
}

State Delta-Ereignis

Inkrementelle Zustandsaktualisierungen mithilfe des JSON-Patchformats, die als Argumente des LLM-Streams-Tools ausgegeben werden:

{
    "type": "STATE_DELTA",
    "delta": [
        {
            "op": "replace",
            "path": "/recipe",
            "value": {
                "title": "Classic Pasta Carbonara",
                "skill_level": "Intermediate",
                "cooking_time": "30 min",
                "ingredients": [
                    {"icon": "🍝", "name": "Spaghetti", "amount": "400g"}
                ],
                "instructions": ["Bring a large pot of salted water to boil"]
            }
        }
    ]
}

Note

Zustandsdeltaereignisse werden in Echtzeit gestreamt, während das LLM die Toolargumente generiert und optimistische UI-Updates ermöglicht. Die Momentaufnahme des endgültigen Zustands wird ausgegeben, wenn das Tool die Ausführung abgeschlossen hat.

Clientimplementierung

Das agent_framework_ag_ui Paket stellt AGUIChatClient für die Verbindung mit AG-UI-Servern bereit, wodurch das Python-Client-Erlebnis dem von .NET angeglichen wird.

"""AG-UI client with state management."""

import asyncio
import json
import os
from typing import Any

from agent_framework import Agent, Message, Role
from agent_framework_ag_ui import AGUIChatClient


async def main():
    """Example client with state tracking."""
    server_url = os.environ.get("AGUI_SERVER_URL", "http://127.0.0.1:8888/")
    print(f"Connecting to AG-UI server at: {server_url}\n")

    # Create AG-UI chat client
    chat_client = AGUIChatClient(endpoint=server_url)

    # Wrap with Agent for convenient API
    agent = Agent(
        name="ClientAgent",
        client=chat_client,
        instructions="You are a helpful assistant.",
    )

    # Get a thread for conversation continuity
    thread = agent.create_session()

    # Track state locally
    state: dict[str, Any] = {}

    try:
        while True:
            message = input("\nUser (:q to quit, :state to show state): ")
            if not message.strip():
                continue

            if message.lower() in (":q", "quit"):
                break

            if message.lower() == ":state":
                print(f"\nCurrent state: {json.dumps(state, indent=2)}")
                continue

            print()
            # Stream the agent response with state
            async for update in agent.run(message, session=thread, stream=True):
                # Handle text content
                if update.text:
                    print(update.text, end="", flush=True)

                # Handle state updates surfaced through AG-UI events.
                for content in update.contents:
                    if content.type == "data" and getattr(content, "media_type", None) == "application/json":
                        print("\n[JSON state payload received]")

            print(f"\n\nCurrent state: {json.dumps(state, indent=2)}")
            print()

    except KeyboardInterrupt:
        print("\n\nExiting...")


if __name__ == "__main__":
    # Install dependencies: pip install agent-framework-ag-ui --pre
    asyncio.run(main())

Wichtige Vorteile

Dies AGUIChatClient bietet Folgendes:

  • Vereinfachte Verbindung: Automatische Verarbeitung der HTTP/SSE-Kommunikation
  • Threadverwaltung: Integrierte Thread-ID-Nachverfolgung für die Unterhaltungskontinuität
  • Agent-Integration: Funktioniert nahtlos mit Agent der vertrauten API
  • Zustandsbehandlung: Automatische Analyse von Zustandsereignissen vom Server
  • Parität mit .NET: Konsistente Erfahrung über verschiedene Sprachen hinweg

Tip

Verwenden Sie AGUIChatClient zusammen mit Agent, um die vollen Vorteile der Funktionen des Agent-Frameworks, wie Unterhaltungsverlauf, Toolausführung und Middleware-Unterstützung, zu nutzen.

Bestätigen des vorhergesagten Zustands

Legen Sie require_confirmation=True für AgentFrameworkAgent fest, wenn vorhergesagte Zustandsänderungen vor der Anwendung auf die Bestätigung durch den Client warten sollen:

recipe_agent = AgentFrameworkAgent(
    agent=agent,
    state_schema={"recipe": {"type": "object", "description": "The current recipe"}},
    predict_state_config={"recipe": {"tool": "update_recipe", "tool_argument": "recipe"}},
    require_confirmation=True,
)

Passen Sie den Bestätigungstext in der Benutzeroberfläche Ihres AG-UI-Clients an, wenn das Bestätigungsereignis gerendert wird.

Beispielinteraktion

Wenn der Server und der Client ausgeführt werden:

User (:q to quit, :state to show state): I want to make a classic Italian pasta carbonara

[Run Started]
[Calling Tool: update_recipe]
[State Updated]
[State Updated]
[State Updated]
[Tool Result: Recipe updated.]
Here's your recipe!
[Run Finished]

============================================================
CURRENT STATE
============================================================

recipe:
  title: Classic Pasta Carbonara
  skill_level: Intermediate
  special_preferences: ['Authentic Italian']
  cooking_time: 30 min
  ingredients:
    - 🍝 Spaghetti: 400g
    - 🥓 Guanciale or bacon: 200g
    - 🥚 Egg yolks: 4
    - 🧀 Pecorino Romano: 100g grated
    - 🧂 Black pepper: To taste
  instructions:
    1. Bring a large pot of salted water to boil
    2. Cut guanciale into small strips and fry until crispy
    3. Beat egg yolks with grated Pecorino and black pepper
    4. Cook spaghetti until al dente
    5. Reserve 1 cup pasta water, then drain pasta
    6. Remove pan from heat, add hot pasta to guanciale
    7. Quickly stir in egg mixture, adding pasta water to create creamy sauce
    8. Serve immediately with extra Pecorino and black pepper

============================================================

Tip

Verwenden Sie den :state Befehl, um den aktuellen Zustand jederzeit während der Unterhaltung anzuzeigen.

Prädiktive Zustandsaktualisierungen in Aktion

Bei Verwendung von vorhersagebasierten Statusaktualisierungen mit predict_state_config empfängt der Client STATE_DELTA Ereignisse, da der LLM die Argumente für Werkzeuge in Echtzeit generiert, bevor das Tool ausgeführt wird.

// Agent starts generating tool call for update_recipe
// Client receives STATE_DELTA events as the recipe argument streams:

// First delta - partial recipe with title
{
  "type": "STATE_DELTA",
  "delta": [{"op": "replace", "path": "/recipe", "value": {"title": "Classic Pasta"}}]
}

// Second delta - title complete with more fields
{
  "type": "STATE_DELTA",
  "delta": [{"op": "replace", "path": "/recipe", "value": {
    "title": "Classic Pasta Carbonara",
    "skill_level": "Intermediate"
  }}]
}

// Third delta - ingredients starting to appear
{
  "type": "STATE_DELTA",
  "delta": [{"op": "replace", "path": "/recipe", "value": {
    "title": "Classic Pasta Carbonara",
    "skill_level": "Intermediate",
    "cooking_time": "30 min",
    "ingredients": [
      {"icon": "🍝", "name": "Spaghetti", "amount": "400g"}
    ]
  }}]
}

// ... more deltas as the LLM generates the complete recipe

Auf diese Weise kann der Client optimistische UI-Updates in Echtzeit anzeigen, während der Agent denkt und den Benutzern sofortiges Feedback gibt.

Zustand mit "Human-in-the-Loop"

Sie können die Zustandsverwaltung mit Genehmigungsworkflows kombinieren, indem Sie Folgendes festlegen require_confirmation=True:

recipe_agent = AgentFrameworkAgent(
    agent=agent,
    state_schema={"recipe": {"type": "object", "description": "The current recipe"}},
    predict_state_config={"recipe": {"tool": "update_recipe", "tool_argument": "recipe"}},
    require_confirmation=True,  # Require approval for state changes
)

Wenn aktiviert:

  1. Statusaktualisierungsstream, wenn der Agent Toolargumente generiert (Predictive Updates via STATE_DELTA Events)
  2. Der Agent pausiert vor der Ausführung des Tools mit einer tool_call Unterbrechung in RUN_FINISHED.outcome.interrupts
  3. Bei Genehmigung wird das Tool ausgeführt und der endgültige Zustand wird über ein STATE_SNAPSHOT-Ereignis ausgegeben.
  4. Wenn dies abgelehnt wird, werden die Änderungen des Vorhersagezustands verworfen.

Erweiterte Zustandsmuster

Komplexer Zustand mit mehreren Feldern

Sie können mehrere Statusfelder mit unterschiedlichen Tools verwalten:

from pydantic import BaseModel


class TaskStep(BaseModel):
    """A single task step."""
    description: str
    status: str = "pending"
    estimated_duration: str = "5 min"


@tool
def generate_task_steps(steps: list[TaskStep]) -> str:
    """Generate task steps for a given task."""
    return f"Generated {len(steps)} steps."


@tool
def update_preferences(preferences: dict[str, Any]) -> str:
    """Update user preferences."""
    return "Preferences updated."


# Configure with multiple state fields
agent_with_multiple_state = AgentFrameworkAgent(
    agent=agent,
    state_schema={
        "steps": {"type": "array", "description": "List of task steps"},
        "preferences": {"type": "object", "description": "User preferences"},
    },
    predict_state_config={
        "steps": {"tool": "generate_task_steps", "tool_argument": "steps"},
        "preferences": {"tool": "update_preferences", "tool_argument": "preferences"},
    },
)

Verwenden von Wildcard-Toolargumenten

Wenn ein Tool komplexe geschachtelte Daten zurückgibt, verwenden Sie "*", um alle Toolargumente dem Zustand zuzuordnen.

@tool
def create_document(title: str, content: str, metadata: dict[str, Any]) -> str:
    """Create a document with title, content, and metadata."""
    return "Document created."


# Map all tool arguments to document state
predict_state_config = {
    "document": {"tool": "create_document", "tool_argument": "*"}
}

Dadurch wird der gesamte Toolaufruf (alle Argumente) dem document Statusfeld zugeordnet.

Bewährte Methoden

Pydantische Modelle verwenden

Definieren sie strukturierte Modelle für die Typsicherheit:

class Recipe(BaseModel):
    """Use Pydantic models for structured, validated state."""
    title: str
    skill_level: SkillLevel
    ingredients: list[Ingredient]
    instructions: list[str]

Vorteile:

  • Typsicherheit: Automatische Validierung von Datentypen
  • Dokumentation: Feldbeschreibungen dienen als Dokumentation
  • IDE-Unterstützung: Automatische Vervollständigung und Typüberprüfung
  • Serialisierung: Automatische JSON-Konvertierung

Statusaktualisierungen abschließen

Schreiben Sie immer den vollständigen Zustand auf, nicht nur die Delta-Werte.

@tool
def update_recipe(recipe: Recipe) -> str:
    """
    You MUST write the complete recipe with ALL fields.
    When modifying a recipe, include ALL existing ingredients and
    instructions plus your changes. NEVER delete existing data.
    """
    return "Recipe updated."

Dadurch wird die Zustandskonsistenz und die richtigen Vorhersageupdates sichergestellt.

Parameternamen abgleichen

Stellen Sie sicher, dass die Toolparameternamen mit der Konfiguration übereinstimmen tool_argument :

# Tool parameter name
def update_recipe(recipe: Recipe) -> str:  # Parameter name: 'recipe'
    ...

# Must match in predict_state_config
predict_state_config = {
    "recipe": {"tool": "update_recipe", "tool_argument": "recipe"}  # Same name
}

Bereitstellen von Kontext in Anweisungen

Schließen Sie klare Anweisungen zur Zustandsverwaltung ein:

agent = Agent(
    instructions="""
    CRITICAL RULES:
    1. You will receive the current recipe state in the system context
    2. To update the recipe, you MUST use the update_recipe tool
    3. When modifying a recipe, ALWAYS include ALL existing data plus your changes
    4. NEVER delete existing ingredients or instructions - only add or modify
    """,
    ...
)

Anpassen der Bestätigungs-UI

Passen Sie Genehmigungs- und Statusbestätigungsmeldungen in Ihrem AG-UI-Client beim Rendern von Bestätigungsereignissen vom Server an.

Nächste Schritte

Sie haben jetzt alle wichtigsten AG-UI Features gelernt! Als Nächstes haben Sie folgende Möglichkeiten:

  • Erkunden der Agent Framework-Dokumentation
  • Erstellen einer vollständigen Anwendung, die alle AG-UI Features kombiniert
  • Bereitstellen Ihres AG-UI-Diensts in die Produktionsumgebung

Zusätzliche Ressourcen

Go AG-UI Zustandsverwaltung kann mit Middleware implementiert werden, die strukturierte message.DataContent Updates zusammen mit normalen Textaktualisierungen ausgibt.

stateSnapshotMiddleware := agent.MiddlewareFunc(func(next agent.RunFunc, ctx context.Context, messages []*message.Message, opts ...agent.Option) iter.Seq2[*agent.ResponseUpdate, error] {
    return func(yield func(*agent.ResponseUpdate, error) bool) {
        for update, err := range next(ctx, messages, opts...) {
            if err != nil {
                yield(nil, err)
                return
            }
            if update != nil {
                // Inspect update contents and yield DataContent snapshots as needed.
            }
            if !yield(update, nil) {
                return
            }
        }
    }
})

a := foundryprovider.NewAgent(endpoint, token, foundryprovider.ModelDeployment(model), foundryprovider.AgentConfig{
    Config: agent.Config{
        Middlewares: []agent.Middleware{stateSnapshotMiddleware},
    },
})

Tip

Siehe das AG-UI-Beispiel zur Zustandsverwaltung für ein vollständig lauffähiges Beispiel.