Gestione dello stato con AG-UI

Questa esercitazione illustra come implementare la gestione dello stato con AG-UI, abilitando la sincronizzazione bidirezionale dello stato tra il client e il server. Questo è essenziale per la creazione di applicazioni interattive come l'interfaccia utente generativa, i dashboard in tempo reale o le esperienze di collaborazione.

Prerequisiti

Prima di iniziare, assicurarsi di comprendere:

Che cos'è Gestione stato?

La gestione dello stato in AG-UI abilita:

  • Stato condiviso: sia il client che il server mantengono una visualizzazione sincronizzata dello stato dell'applicazione
  • Sincronizzazione bidirezionale: lo stato può essere aggiornato da client o server
  • Aggiornamenti in tempo reale: le modifiche vengono trasmessi immediatamente usando gli eventi di stato
  • Aggiornamenti predittivi: il flusso degli aggiornamenti di stato mentre LLM genera parametri dello strumento (interfaccia utente ottimista)
  • Structured Data: Lo stato segue uno schema JSON per la convalida

Casi d'uso

La gestione dello stato è utile per:

  • Interfaccia utente generativa: creare componenti dell'interfaccia utente in base allo stato controllato dall'agente
  • Compilazione modulo: Agent popola i campi modulo durante la raccolta di informazioni
  • Monitoraggio dello stato d'avanzamento: mostra lo stato d'avanzamento in tempo reale delle operazioni su più fasi
  • Dashboard interattivi: visualizzare i dati aggiornati durante l'elaborazione dell'agente
  • Modifica collaborativa: più utenti visualizzano aggiornamenti coerenti dello stato

Creazione di agenti State-Aware in C#

La gestione dello stato nell'integrazione .NET AG-UI è dichiarativa: l'agente espone gli strumenti ordinari che restituiscono gli oggetti di stato e indica al livello di hosting i risultati dello strumento che diventano AG-UI eventi di stato configurando un oggetto AGUIStreamOptions. Non si scrive un agente personalizzato o si genera il contenuto del protocollo a mano.

Definire il modello di stato

Prima di tutto, definire le classi per la struttura dello stato:

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;

Generare uno snapshot di stato da uno strumento

Esporre uno strumento che restituisce lo stato completo. L'agente lo chiama ogni volta che la ricetta deve cambiare. Il livello di hosting trasforma il risultato dello strumento in un STATE_SNAPSHOT evento quando viene mappato:

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

Creare l'agente

Creare l'agente direttamente dal client di chat con ChatClientAgentOptions. Inserire il prompt e gli strumenti di sistema in 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],
        },
    });

Avvertimento

DefaultAzureCredential è utile per lo sviluppo, ma richiede un'attenta considerazione nell'ambiente di produzione. Nell'ambiente di produzione prendere in considerazione l'uso di credenziali specifiche ,ad esempio ManagedIdentityCredential, per evitare problemi di latenza, probe di credenziali indesiderate e potenziali rischi per la sicurezza dai meccanismi di fallback.

Eseguire il mapping del risultato dello strumento a un evento di stato

Creare un AGUIStreamOptionsoggetto , registrare il nome dello strumento come snapshot dello stato e collegarlo ai metadati dell'endpoint. MapAGUIServer legge le opzioni del flusso dall'endpoint (o dall'inserimento IOptions<AGUIStreamOptions> delle dipendenze) e genera gli eventi di stato per l'utente:

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();

Questo è l'intero server. Non esiste contenuto personalizzato DelegatingAIAgent e nessun protocollo da compilare: lo strumento restituisce l'oggetto di stato, MapResultAsStateSnapshot trasforma ogni risultato in un STATE_SNAPSHOTe il framework lo trasmette al client.

Lettura dello stato del client

La ricetta vive sul cliente. Quando il client invia un turno, include il relativo stato corrente nel AG-UI RunAgentInput. Recuperarlo dalla richiesta ChatOptions con e leggere RunAgentInput.State (a JsonElementTryGetRunAgentInput ):

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 legge l'input che il livello di hosting ha accodato su ChatOptions.AdditionalProperties. Non tocca mai direttamente quel dizionario. Assegnare al modello la ricetta corrente anteponendola come messaggio di sistema prima dell'esecuzione dell'agente (ad esempio, da un leggero DelegatingAIAgent che inserisce solo il contesto e delega l'esecuzione), quindi modifica la compilazione sullo stato esistente invece di iniziare da zero.

Concetti chiave

  • Strumenti restituiscono lo stato: uno strumento restituisce l'oggetto stato; non si costruiscono mai eventi AG-UI se stessi.
  • Mapping dichiarativo: AGUIStreamOptions.MapResultAsStateSnapshot(toolName) / MapResultAsStateDelta(toolName) eseguire il mapping di un risultato dello strumento a un STATE_SNAPSHOT / STATE_DELTA evento.
  • Collegamento dell'endpoint: collegare le opzioni del flusso con .WithMetadata(streamOptions) in MapAGUIServero registrare IOptions<AGUIStreamOptions> in inserimento di dipendenze.
  • Stato di lettura: ChatOptions.TryGetRunAgentInput(out var input) recupera ; RunAgentInputinput.State è lo stato corrente del client come .JsonElement

Delta di stato con l'interfaccia utente generativa agenti

MapResultAsStateSnapshot sostituisce l'intero stato in ogni turno. Per le modifiche incrementali, eseguire il mapping di un risultato dello strumento a un STATE_DELTA con MapResultAsStateDelta, restituendo un documento patch JSON .

Uno scenario comune è l'interfaccia utente generativa agente: l'agente compila un piano, quindi aggiorna lo stato dei singoli passaggi man mano che funziona. create_plan invia il piano completo come snapshot; update_plan_step invia solo i campi modificati come delta.

Definire l'enumerazione del modello di piano e dello stato:

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; }
}

Lo create_plan strumento restituisce il piano completo. update_plan_step Restituisce un elenco di operazioni patch JSON:

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 }
    ];
}

Registrare entrambi gli strumenti nell'agente (in AllowMultipleToolCalls = false modo che il modello aggiorni un passaggio alla volta) e mappare ogni risultato dello strumento all'evento di stato corrispondente: create_plan a uno snapshot, update_plan_step a un 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);

Annotazioni

STATE_SNAPSHOT sostituisce l'intero stato; STATE_DELTA applica una patch JSON allo stato esistente. Inviare uno snapshot durante la configurazione o la reimpostazione dello stato e i delta per le modifiche incrementali.

Per un modello correlato che trasmette gli argomenti di uno strumento allo stato durante la generazione del modello, continuare con gli aggiornamenti dello stato predittivo di seguito.

Aggiornamenti dello stato predittivo

Gli aggiornamenti dello stato predittivo consentono all'interfaccia utente di reagire a una chiamata dello strumento mentre i relativi argomenti vengono ancora generati, invece di attendere il completamento dello strumento. Quando il modello trasmette gli argomenti per uno strumento, il server converte tali argomenti parziali in snapshot di stato e li invia al client. Il client esegue immediatamente il rendering di ogni snapshot, dando all'utente un'anteprima ottimistica e in tempo reale. Ad esempio, un editor di documenti mostra il testo visualizzato in tempo reale, quindi chiede all'utente di confermare la modifica al termine del modello.

Annotazioni

Questo scenario esegue il mapping manuale dell'endpoint e usa l'predefinito TypedResults.ServerSentEvents(...), che richiede .NET 10.0 o versione successiva.

Funzionamento

A differenza dello scenario con stato condiviso , in cui viene eseguito uno strumento e il relativo risultato diventa uno snapshot, lo scenario predittivo intercetta la chiamata dello strumento prima dell'esecuzione e trasmette l'argomento allo stato:

  1. L'agente dichiara uno write_document_local strumento. Il modello lo chiama con il testo completo del documento come document argomento.
  2. Lo strumento non viene eseguito sul lato server. Un mapping intercetta invece AGUIStreamOptionsMapCall la chiamata.
  3. Il mapping genera una serie di STATE_SNAPSHOT eventi, ognuno con un prefisso progressivamente più lungo del documento, in modo che il client visualizzi il flusso di testo.
  4. Completa quindi la chiamata allo strumento con un TOOL_CALL_RESULT evento e inserisce una chiamata dello strumento sul lato confirm_changes client in modo che il client possa richiedere all'utente di approvare.
  5. Il client esegue il rendering di ogni snapshot e mostra la richiesta di conferma/rifiuto.

Poiché il mapping produce il risultato stesso dello strumento, lo strumento del documento viene dichiarato ma mai richiamato. Il client di chat viene compilato senza chiamata di funzione.

Definire il modello di stato

Il modello di stato descrive la forma di cui esegue il rendering il client. Usare JsonPropertyName in modo che i nomi delle proprietà corrispondano a quanto previsto dal client:

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;

Dichiarare lo strumento documento

La firma dello strumento è ciò che il modello riempie. Viene dichiarato in modo che il modello lo chiami, ma il risultato viene prodotto dal mapping del flusso, non eseguendo il corpo del metodo:

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

Configurare il mapping del flusso predittivo

Registrare un MapCall mapping per lo strumento. Quando il modello chiama write_document_local, il mapping legge l'argomento trasmesso document , genera snapshot progressivi StateSnapshotEvent , completa la chiamata allo strumento e inserisce una confirm_changes chiamata allo strumento sul lato client:

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;
    });
}

Annotazioni

Ogni snapshot contiene il documento completo fino a quel punto, quindi il client esegue sempre il rendering di una visualizzazione coerente anche se manca un aggiornamento intermedio.

Eseguire il mapping dell'endpoint

Poiché il mapping produce il risultato stesso dello strumento, compilare il client di chat senza chiamate di funzione e trasmettere direttamente la pipeline di AG-UI: adattare l'ingresso RunAgentInput con ToChatRequestContext, chiamare GetStreamingResponseAsynce convertire gli aggiornamenti con 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();

Avvertimento

DefaultAzureCredential è utile per lo sviluppo, ma richiede un'attenta considerazione nell'ambiente di produzione. Nell'ambiente di produzione prendere in considerazione l'uso di credenziali specifiche ,ad esempio ManagedIdentityCredential, per evitare problemi di latenza, probe di credenziali indesiderate e potenziali rischi per la sicurezza dai meccanismi di fallback.

Annotazioni

confirm_changes è uno strumento lato client . Il mapping del flusso lo richiede e il client esegue il rendering del prompt di approvazione. Vedere Human-in-the-Loop per il modello di strumento sul lato client.

Concetti chiave predittiva

  • AGUIStreamOptions.MapCall: intercetta una chiamata allo strumento (prima dell'esecuzione) e restituisce gli eventi AG-UI da generare per esso.
  • FunctionCallContent.Arguments: argomenti dello strumento trasmessi. Leggere Arguments["document"] per ottenere il testo man mano che il modello lo produce.
  • StateSnapshotEvent: ogni snapshot contiene finora il prefisso completo del documento, generando l'effetto di streaming ottimistico.
  • ToChatRequestContext / AsAGUIEventStreamAsync: la pipeline di streaming AG-UI che adatta un oggetto RunAgentInput a una richiesta di chat e converte nuovamente gli aggiornamenti della risposta in eventi AG-UI.
  • confirm_changes: chiamata dello strumento sul lato client inserita dopo la scrittura del documento, in modo che l'utente possa approvare il risultato.

Rendering nel client

Un toolkit dell'interfaccia utente, ad esempio CopilotKit , sottoscrive gli snapshot di stato ed esegue di nuovo il rendering del documento su ognuno di essi, quindi mostra la richiesta di conferma o rifiuto all'arrivo della chiamata allo confirm_changes strumento. È possibile visualizzare questo scenario in esecuzione nel AG-UI Dojo.

Definire i modelli di stato

Innanzitutto, definisci i modelli Pydantic per la struttura dello stato. In questo modo si garantisce la sicurezza e la convalida dei tipi:

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

Schema dello stato

Definire uno schema di stato per specificare la struttura e i tipi dello stato:

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

Annotazioni

Lo schema dello stato usa un formato semplice con type e facoltativo description. La struttura effettiva è definita dai modelli Pydantic.

Aggiornamenti dello stato predittivo

Gli aggiornamenti dello stato predittivo trasmettono gli argomenti del tool allo stato man mano che l'LLM li genera, consentendo aggiornamenti ottimistici dell'interfaccia utente.

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

Questa configurazione mappa il campo di stato recipe all'argomento recipe dello strumento update_recipe. Quando l'agente chiama lo strumento, gli argomenti vengono trasmessi allo stato in tempo reale man mano che l'LLM li genera.

Definire lo strumento di aggiornamento dello stato

Creare una funzione dello strumento che accetta il modello Pydantic:

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

Importante

Il nome del parametro della funzione strumento (recipe) deve corrispondere a tool_argument in predict_state_config.

Creare l'agente con Gestione stato

Ecco un'implementazione completa del server con la gestione dello stato:

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

Concetti chiave

  • Modelli Pydantic: definire lo stato strutturato con tipizzazione sicura e convalida dei tipi
  • Schema dello stato: formato semplice che specifica i tipi di campo di stato
  • Configurazione dello stato predittivo: esegue il mapping dei campi di stato agli argomenti dello strumento per gli aggiornamenti in streaming
  • Inserimento dello stato: lo stato corrente viene inserito automaticamente come messaggi di sistema per fornire il contesto
  • Aggiornamenti completi: gli strumenti devono scrivere lo stato completo, non solo i delta
  • Strategia di conferma: personalizzare i messaggi di approvazione per il dominio (ricetta, documento, pianificazione delle attività e così via)

Comprendere gli eventi di stato

Evento snapshot dello stato

Una istantanea completa dello stato corrente, emessa quando lo strumento è completato:

{
    "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"
            ]
        }
    }
}

Evento di Stato Delta

Aggiornamenti incrementali dello stato usando il formato JSON Patch, generati come argomenti per lo strumento di streaming LLM.

{
    "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"]
            }
        }
    ]
}

Annotazioni

Il flusso di eventi degli stati delta avviene in tempo reale mentre il modello di linguaggio genera gli argomenti dello strumento, fornendo aggiornamenti ottimistici della UI. Lo snapshot dello stato finale viene generato al termine dell'esecuzione dello strumento.

Implementazione del client

Il agent_framework_ag_ui pacchetto consente AGUIChatClient di connettersi ai server AG-UI, portando l'esperienza client Python alla parità con .NET:

"""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())

Vantaggi principali

Fornisce AGUIChatClient :

  • Connessione semplificata: gestione automatica delle comunicazioni HTTP/SSE
  • Gestione thread: tracciamento dell'ID thread integrato per la continuità della conversazione
  • Integrazione dell'agente: funziona perfettamente con Agent per un'API familiare
  • Gestione dello stato: analisi automatica degli eventi di stato dal server
  • Parità con .NET: esperienza coerente tra i linguaggi

Suggerimento

Usare AGUIChatClient con Agent per ottenere il massimo vantaggio delle funzionalità del framework agente, ad esempio la cronologia delle conversazioni, l'esecuzione degli strumenti e il supporto del middleware.

Conferma dello stato stimato

Impostare require_confirmation=True su AgentFrameworkAgent quando le modifiche dello stato stimate devono attendere la conferma del client prima di essere applicate:

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

Personalizza il testo di conferma nell'interfaccia utente del client AG-UI quando viene visualizzato l'evento di conferma.

Interazione di esempio

Con il server e il client in esecuzione:

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

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

Suggerimento

Usare il :state comando per visualizzare lo stato corrente in qualsiasi momento durante la conversazione.

Aggiornamenti predittivi dello stato in azione

Quando si usano gli aggiornamenti dello stato predittivo con predict_state_config, il client riceve STATE_DELTA eventi perché LLM genera argomenti dello strumento in tempo reale, prima che lo strumento venga eseguito:

// 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

In questo modo il client può visualizzare gli aggiornamenti ottimistici dell'interfaccia utente in tempo reale man mano che l'agente sta pensando, fornendo feedback immediato agli utenti.

Stato con umano nel ciclo

È possibile combinare la gestione dello stato con i flussi di lavoro di approvazione impostando 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
)

Se abilitato/a:

  1. Flusso degli aggiornamenti di stato mentre l'agente genera gli argomenti dello strumento (aggiornamenti predittivi tramite gli eventi STATE_DELTA)
  2. L'agent si interrompe prima di eseguire lo strumento con un'tool_call interruzione in RUN_FINISHED.outcome.interrupts
  3. Se approvato, lo strumento esegue e lo stato finale viene generato (tramite STATE_SNAPSHOT evento)
  4. Se rifiutata, le modifiche dello stato predittivo vengono rimosse

Modelli di stato avanzati

Stato complesso con più campi

È possibile gestire più campi di stato con diversi strumenti:

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"},
    },
)

Uso degli argomenti dello strumento con caratteri jolly

Quando uno strumento restituisce dati complessi e annidati, utilizzare "*" per mappare tutti gli argomenti dello strumento allo stato:

@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": "*"}
}

Esegue il mapping dell'intera chiamata dello strumento (tutti gli argomenti) al document campo di stato.

Migliori pratiche

Utilizzare modelli Pydantic

Definire modelli strutturati per la sicurezza dei tipi:

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

Vantaggi:

  • Sicurezza dei tipi: convalida automatica dei tipi di dati
  • Documentazione: Le descrizioni dei campi fungono da documentazione
  • Supporto dell'IDE: completamento automatico e controllo del tipo
  • Serializzazione: conversione JSON automatica

Aggiornamenti dello stato completi

Scrivere sempre lo stato completo anziché solo i delta.

@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."

In questo modo si garantisce la coerenza dello stato e gli aggiornamenti predittivi appropriati.

Abbinare i nomi dei parametri

Verificare che i nomi dei parametri degli strumenti corrispondano alla tool_argument configurazione:

# 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
}

Fornire contesto nelle istruzioni

Includere istruzioni chiare sulla gestione dello stato:

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

Personalizzare l'interfaccia utente di conferma

Personalizzare i messaggi di approvazione e conferma dello stato nel client AG-UI durante il rendering degli eventi di conferma dal server.

Operazioni successive

A questo punto sono state apprese tutte le funzionalità di base AG-UI. A questo punto è possibile:

  • Esplorare la documentazione di Agent Framework
  • Creare un'applicazione completa che combina tutte le funzionalità di AG-UI
  • Distribuire il servizio AG-UI nell'ambiente di produzione

Risorse aggiuntive

La gestione dello stato in Go AG-UI può essere implementata con middleware che emette aggiornamenti strutturati message.DataContent insieme ai normali aggiornamenti di testo.

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},
    },
})

Suggerimento

Vedere l'esempio di gestione dello statoAG-UI per un esempio eseguibile completo.