Condividi tramite


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#

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
internal sealed class RecipeResponse
{
    [JsonPropertyName("recipe")]
    public RecipeState Recipe { get; set; } = new();
}

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

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

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

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

    [JsonPropertyName("prep_time_minutes")]
    public int PrepTimeMinutes { get; set; }

    [JsonPropertyName("cook_time_minutes")]
    public int CookTimeMinutes { get; set; }

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

// JSON serialization context
[JsonSerializable(typeof(RecipeResponse))]
[JsonSerializable(typeof(RecipeState))]
[JsonSerializable(typeof(System.Text.Json.JsonElement))]
internal sealed partial class RecipeSerializerContext : JsonSerializerContext;

Implementare il middleware di gestione dello stato

Creare middleware che gestisce la gestione dello stato rilevando quando il client invia lo stato e coordinando le risposte dell'agente:

using System.Runtime.CompilerServices;
using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;

internal sealed class SharedStateAgent : DelegatingAIAgent
{
    private readonly JsonSerializerOptions _jsonSerializerOptions;

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

    protected override Task<AgentResponse> RunCoreAsync(
        IEnumerable<ChatMessage> messages,
        AgentSession? session = null,
        AgentRunOptions? options = null,
        CancellationToken cancellationToken = default)
    {
        return this.RunStreamingAsync(messages, session, options, cancellationToken)
            .ToAgentResponseAsync(cancellationToken);
    }

    protected override async IAsyncEnumerable<AgentResponseUpdate> RunCoreStreamingAsync(
        IEnumerable<ChatMessage> messages,
        AgentSession? session = null,
        AgentRunOptions? options = null,
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        // Check if the client sent state in the request
        if (options is not ChatClientAgentRunOptions { ChatOptions.AdditionalProperties: { } properties } chatRunOptions ||
            !properties.TryGetValue("ag_ui_state", out object? stateObj) ||
            stateObj is not JsonElement state ||
            state.ValueKind != JsonValueKind.Object)
        {
            // No state management requested, pass through to inner agent
            await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, session, options, cancellationToken).ConfigureAwait(false))
            {
                yield return update;
            }
            yield break;
        }

        // Check if state has properties (not empty {})
        bool hasProperties = false;
        foreach (JsonProperty _ in state.EnumerateObject())
        {
            hasProperties = true;
            break;
        }

        if (!hasProperties)
        {
            // Empty state - treat as no state
            await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, session, options, cancellationToken).ConfigureAwait(false))
            {
                yield return update;
            }
            yield break;
        }

        // First run: Generate structured state update
        var firstRunOptions = new ChatClientAgentRunOptions
        {
            ChatOptions = chatRunOptions.ChatOptions.Clone(),
            AllowBackgroundResponses = chatRunOptions.AllowBackgroundResponses,
            ContinuationToken = chatRunOptions.ContinuationToken,
            ChatClientFactory = chatRunOptions.ChatClientFactory,
        };

        // Configure JSON schema response format for structured state output
        firstRunOptions.ChatOptions.ResponseFormat = ChatResponseFormat.ForJsonSchema<RecipeResponse>(
            schemaName: "RecipeResponse",
            schemaDescription: "A response containing a recipe with title, skill level, cooking time, preferences, ingredients, and instructions");

        // Add current state to the conversation - state is already a JsonElement
        ChatMessage stateUpdateMessage = new(
            ChatRole.System,
            [
                new TextContent("Here is the current state in JSON format:"),
                new TextContent(JsonSerializer.Serialize(state, this._jsonSerializerOptions.GetTypeInfo(typeof(JsonElement)))),
                new TextContent("The new state is:")
            ]);

        var firstRunMessages = messages.Append(stateUpdateMessage);

        // Collect all updates from first run
        var allUpdates = new List<AgentResponseUpdate>();
        await foreach (var update in this.InnerAgent.RunStreamingAsync(firstRunMessages, session, firstRunOptions, cancellationToken).ConfigureAwait(false))
        {
            allUpdates.Add(update);

            // Yield all non-text updates (tool calls, etc.)
            bool hasNonTextContent = update.Contents.Any(c => c is not TextContent);
            if (hasNonTextContent)
            {
                yield return update;
            }
        }

        var response = allUpdates.ToAgentResponse();

        // Try to deserialize the structured state response
        JsonElement stateSnapshot;
        try
        {
            stateSnapshot = JsonSerializer.Deserialize<JsonElement>(response.Text, this._jsonSerializerOptions);
        }
        catch (JsonException)
        {
            yield break;
        }

        // Serialize and emit as STATE_SNAPSHOT via DataContent
        byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes(
            stateSnapshot,
            this._jsonSerializerOptions.GetTypeInfo(typeof(JsonElement)));
        yield return new AgentResponseUpdate
        {
            Contents = [new DataContent(stateBytes, "application/json")]
        };

        // Second run: Generate user-friendly summary
        var secondRunMessages = messages.Concat(response.Messages).Append(
            new ChatMessage(
                ChatRole.System,
                [new TextContent("Please provide a concise summary of the state changes in at most two sentences.")]));

        await foreach (var update in this.InnerAgent.RunStreamingAsync(secondRunMessages, session, options, cancellationToken).ConfigureAwait(false))
        {
            yield return update;
        }
    }
}

Configurare l'agente con Gestione stato

using Microsoft.Agents.AI;
using Azure.AI.Projects;
using Azure.Identity;

AIAgent CreateRecipeAgent(JsonSerializerOptions jsonSerializerOptions)
{
    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.");

    // Create base agent
    AIAgent baseAgent = new AIProjectClient(
        new Uri(endpoint),
        new DefaultAzureCredential())
        .AsAIAgent(
            model: deploymentName,
            name: "RecipeAgent",
        instructions: """
            You are a helpful recipe assistant. When users ask you to create or suggest a recipe,
            respond with a complete RecipeResponse JSON object that includes:
            - recipe.title: The recipe name
            - recipe.cuisine: Type of cuisine (e.g., Italian, Mexican, Japanese)
            - recipe.ingredients: Array of ingredient strings with quantities
            - recipe.steps: Array of cooking instruction strings
            - recipe.prep_time_minutes: Preparation time in minutes
            - recipe.cook_time_minutes: Cooking time in minutes
            - recipe.skill_level: One of "beginner", "intermediate", or "advanced"

            Always include all fields in the response. Be creative and helpful.
            """);

    // Wrap with state management middleware
    return new SharedStateAgent(baseAgent, jsonSerializerOptions);
}

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.

Effettuare la mappatura dell'endpoint dell'agente

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

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

WebApplication app = builder.Build();

var jsonOptions = app.Services.GetRequiredService<IOptions<Microsoft.AspNetCore.Http.Json.JsonOptions>>().Value;
AIAgent recipeAgent = CreateRecipeAgent(jsonOptions.SerializerOptions);
app.MapAGUI("/", recipeAgent);

await app.RunAsync();

Concetti chiave

  • Rilevamento dello stato: il middleware verifica la presenza di ag_ui_state in ChatOptions.AdditionalProperties per rilevare quando il client richiede la gestione dello stato
  • Risposta a Due Fasi: genera prima uno stato strutturato (schema JSON), poi un riepilogo descrittivo
  • Modelli di stato strutturati: definire classi C# per la struttura di stato con nomi di proprietà JSON
  • Formato di risposta dello schema JSON: usare ChatResponseFormat.ForJsonSchema<T>() per garantire un output strutturato
  • Eventi STATE_SNAPSHOT: emessi come DataContent con tipo di media application/json, che il framework AG-UI converte automaticamente in eventi STATE_SNAPSHOT
  • Contesto di stato: lo stato corrente viene inserito come messaggio di sistema per fornire contesto all'agente

Funzionamento

  1. Il client invia una richiesta con stato in ChatOptions.AdditionalProperties["ag_ui_state"]
  2. Il middleware rileva lo stato ed esegue la prima esecuzione con il formato di risposta dello schema JSON
  3. Il middleware aggiunge lo stato corrente come contesto in un messaggio di sistema
  4. Agent genera un aggiornamento dello stato strutturato corrispondente al modello di stato
  5. Il middleware serializza lo stato e lo emette come DataContent (evento diventa STATE_SNAPSHOT)
  6. Il middleware esegue la seconda esecuzione per generare un riepilogo descrittivo
  7. Il client riceve sia lo snapshot dello stato che il riepilogo del linguaggio naturale

Suggerimento

L'approccio in due fasi separa la gestione dello stato dalla comunicazione degli utenti. La prima fase garantisce aggiornamenti strutturati e affidabili dello stato mentre la seconda fase fornisce feedback in linguaggio naturale all'utente.

Implementazione del client (C#)

Importante

L'implementazione del client C# non è inclusa in questa esercitazione. La gestione dello stato lato server è completa, ma i client devono:

  1. Inizializzare lo stato con un oggetto vuoto (non Null): RecipeState? currentState = new RecipeState();
  2. Inviare lo stato come DataContent in un ChatRole.System messaggio
  3. Ricevi snapshot degli stati come DataContent con mediaType = "application/json"

Il livello di hosting AG-UI estrae automaticamente lo stato da DataContent e lo inserisce in ChatOptions.AdditionalProperties["ag_ui_state"] come JsonElement.

Per un esempio di implementazione client completo, vedere il modello client Python riportato di seguito che illustra il flusso di stato bidirezionale completo.

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,
    RecipeConfirmationStrategy,
    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"},
    },
    confirmation_strategy=RecipeConfirmationStrategy(),
)

# 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

import jsonpatch
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(server_url=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
                for content in update.contents:
                    # STATE_SNAPSHOT events come as DataContent with application/json
                    if hasattr(content, 'media_type') and content.media_type == 'application/json':
                        # Parse state snapshot
                        state_data = json.loads(content.data.decode() if isinstance(content.data, bytes) else content.data)
                        state = state_data
                        print("\n[State Snapshot Received]")

                    # STATE_DELTA events are handled similarly
                    # Apply JSON Patch deltas to maintain state
                    if hasattr(content, 'delta') and content.delta:
                        patch = jsonpatch.JsonPatch(content.delta)
                        state = patch.apply(state)
                        print("\n[State Delta Applied]")

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

Uso delle strategie di conferma

Il confirmation_strategy parametro consente di personalizzare i messaggi di approvazione per il dominio:

from agent_framework_ag_ui import RecipeConfirmationStrategy

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

Strategie disponibili:

  • DefaultConfirmationStrategy() - Messaggi generici per qualsiasi agente
  • RecipeConfirmationStrategy() - Messaggi specifici della ricetta
  • DocumentWriterConfirmationStrategy() - Messaggi di modifica dei documenti
  • TaskPlannerConfirmationStrategy() - Messaggi di pianificazione delle attività

È anche possibile creare strategie personalizzate ereditando da ConfirmationStrategy e implementando i metodi necessari.

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
    confirmation_strategy=RecipeConfirmationStrategy(),
)

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'agente richiede l'approvazione prima di eseguire lo strumento (tramite FUNCTION_APPROVAL_REQUEST evento)
  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
    """,
    ...
)

Usare strategie di conferma

Personalizzare i messaggi di approvazione per il dominio:

from agent_framework_ag_ui import RecipeConfirmationStrategy

recipe_agent = AgentFrameworkAgent(
    agent=agent,
    confirmation_strategy=RecipeConfirmationStrategy(),  # Domain-specific messages
)

Passaggi successivi

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