Udostępnij za pośrednictwem


Zarządzanie stanem za pomocą AG-UI

W tym samouczku pokazano, jak zaimplementować zarządzanie stanem za pomocą AG-UI, umożliwiając dwukierunkową synchronizację stanu między klientem a serwerem. Jest to niezbędne do tworzenia interaktywnych aplikacji, takich jak generatywny interfejs użytkownika, pulpity nawigacyjne w czasie rzeczywistym lub doświadczenia współpracy.

Wymagania wstępne

Przed rozpoczęciem upewnij się, że rozumiesz:

Co to jest zarządzanie stanem?

Zarządzanie stanem w AG-UI umożliwia:

  • Stan udostępniony: zarówno klient, jak i serwer zachowują zsynchronizowany widok stanu aplikacji
  • Synchronizacja dwukierunkowa: stan można zaktualizować z poziomu klienta lub serwera
  • Aktualizacje w czasie rzeczywistym: zmiany są przesyłane strumieniowo natychmiast przy użyciu zdarzeń stanowych
  • Aktualizacje predykcyjne: strumień aktualizacji stanu w miarę generowania argumentów narzędzi przez moduł LLM (optymistyczny interfejs użytkownika)
  • Dane ustrukturyzowane: stan jest zgodny ze schematem JSON na potrzeby walidacji

Przypadki użycia

Zarządzanie stanem jest cenne dla:

  • Generatywny interfejs użytkownika: tworzenie elementów interfejsu użytkownika na podstawie stanu kontrolowanego przez agenta
  • Kompilowanie formularzy: Agent wypełnia pola formularza podczas zbierania informacji
  • Śledzenie postępu: wyświetlanie postępu operacji wieloetapowych w czasie rzeczywistym
  • Interaktywne pulpity nawigacyjne: wyświetlanie danych aktualizowanych podczas przetwarzania przez agenta
  • Edytowanie zespołowe: wielu użytkowników widzi te spójne aktualizacje stanu

Tworzenie agentów State-Aware w języku C#

Definiowanie modelu stanu

Najpierw zdefiniuj klasy dla struktury stanu:

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;

Implementowanie oprogramowania pośredniczącego zarządzania stanem

Utwórz oprogramowanie pośredniczące obsługujące zarządzanie stanem, wykrywając, kiedy klient wysyła stan i koordynuje odpowiedzi agenta:

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

Konfigurowanie agenta za pomocą zarządzania stanem

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

Ostrzeżenie

DefaultAzureCredential jest wygodne do programowania, ale wymaga starannego rozważenia w środowisku produkcyjnym. W środowisku produkcyjnym rozważ użycie określonego poświadczenia (np. ManagedIdentityCredential), aby uniknąć problemów z opóźnieniami, niezamierzonego sondowania poświadczeń i potencjalnych zagrożeń bezpieczeństwa wynikających z mechanizmów awaryjnych.

Mapuj punkt końcowy agenta

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

Kluczowe pojęcia

  • Wykrywanie stanu: oprogramowanie pośredniczące sprawdza ag_ui_state w ChatOptions.AdditionalProperties w celu wykrycia, kiedy klient żąda zarządzania stanem.
  • Odpowiedź Dwufazowa: najpierw generuje ustrukturyzowany stan (schemat JSON), a następnie generuje podsumowanie przyjazne dla użytkownika
  • Modele stanu strukturalnego: definiowanie klas języka C# dla struktury stanu przy użyciu nazw właściwości JSON
  • Format odpowiedzi schematu JSON: użyj ChatResponseFormat.ForJsonSchema<T>() aby zapewnić ustrukturyzowane wyjście
  • STATE_SNAPSHOT Zdarzenia: emitowane jako DataContent z application/json typem nośnika, który platforma AG-UI automatycznie konwertuje na zdarzenia STATE_SNAPSHOT
  • Kontekst stanu: bieżący stan jest wstrzykiwany jako komunikat systemowy w celu zapewnienia kontekstu agentowi

Jak to działa

  1. Klient wysyła żądanie zawierające stan w ChatOptions.AdditionalProperties["ag_ui_state"]
  2. Oprogramowanie pośredniczące wykrywa stan i wykonuje pierwsze uruchomienie przy użyciu formatu odpowiedzi schematu JSON
  3. Oprogramowanie pośredniczące dodaje bieżący stan jako kontekst w komunikacie systemowym
  4. Agent generuje aktualizację stanu strukturalnego zgodną z modelem stanu
  5. Oprogramowanie pośredniczące serializuje stan i emituje jako DataContent (co staje się zdarzeniem STATE_SNAPSHOT)
  6. Oprogramowanie pośredniczące wykonuje drugie uruchomienie w celu wygenerowania przyjaznego dla użytkownika podsumowania
  7. Klient otrzymuje zarówno migawkę stanu, jak i podsumowanie języka naturalnego

Wskazówka

Podejście dwufazowe oddziela zarządzanie stanem od komunikacji użytkowników. Pierwsza faza zapewnia ustrukturyzowane, niezawodne aktualizacje stanu, podczas gdy druga faza dostarcza użytkownikowi informacji zwrotnej w języku naturalnym.

Implementacja klienta (C#)

Ważne

Implementacja klienta języka C# nie jest zawarta w tym samouczku. Zarządzanie stanem po stronie serwera zostało ukończone, ale klienci muszą:

  1. Zainicjuj stan pustym obiektem (nie null): RecipeState? currentState = new RecipeState();
  2. Wyślij stan jako DataContent w ChatRole.System komunikacie
  3. Odbieranie migawek stanu w następujący sposób:DataContentmediaType = "application/json"

Warstwa hostingu AG-UI automatycznie wyodrębnia stan z DataContent i umieszcza go w ChatOptions.AdditionalProperties["ag_ui_state"] jako JsonElement.

Pełny przykład implementacji klienta można znaleźć w poniższym wzorcu klienta języka Python, który pokazuje pełny przepływ stanu dwukierunkowego.

Definiowanie modeli stanu

Najpierw zdefiniuj modele Pydantic dla struktury stanu. Zapewnia to bezpieczeństwo i walidację typów:

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

Schemat stanu

Zdefiniuj schemat stanu, aby określić strukturę i typy stanu:

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

Uwaga / Notatka

Schemat stanu używa prostego formatu z type i opcjonalnym description. Rzeczywista struktura jest definiowana przez modele Pydantic.

Aktualizacje stanu predykcyjnego

Argumenty narzędzia strumienia aktualizacji stanu predykcyjnego są aktualizowane do stanu w miarę ich generowania przez moduł LLM, co umożliwia optymistyczne aktualizacje interfejsu użytkownika:

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

Konfiguracja przypisuje pole stanu recipe do argumentu recipe narzędzia update_recipe. Gdy agent wywołuje narzędzie, argumenty są strumieniowo przekazywane do stanu w czasie rzeczywistym, gdy LLM je generuje.

Definiowanie narzędzia do aktualizacji stanu

Utwórz funkcję narzędzia, która akceptuje model 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."

Ważne

Nazwa parametru funkcji narzędzia (recipe) musi być zgodna z elementem tool_argument w pliku predict_state_config.

Tworzenie agenta za pomocą zarządzania stanem

Oto kompletna implementacja serwera z zarządzaniem stanem:

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

Kluczowe pojęcia

  • Modele Pydantic: Definiowanie stanu strukturalnego z bezpieczeństwem typów i walidacją
  • Schemat stanu: prosty format określający typy pól stanu
  • Konfiguracja stanu predykcyjnego: mapuje pola stanu na argumenty narzędzi na potrzeby aktualizacji przesyłania strumieniowego
  • Wstrzykiwanie stanu: bieżący stan jest automatycznie wstrzykiwany jako komunikaty systemowe w celu zapewnienia kontekstu
  • Pełne aktualizacje: Narzędzia muszą zapisywać pełny stan, a nie tylko zmiany delta
  • Strategia potwierdzenia: Dostosowywanie komunikatów zatwierdzenia dla domeny (przepis, dokument, planowanie zadań itp.)

Opis zdarzeń stanu

Zdarzenie przechwycenia stanu

Kompletna migawka bieżącego stanu, emitowana po zakończeniu pracy narzędzia:

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

Zdarzenie zmiany stanu

Aktualizacje stanu przyrostowego w formacie JSON Patch, emitowane jako argumenty narzędzia strumieni 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"]
            }
        }
    ]
}

Uwaga / Notatka

Strumień zdarzeń różnicowych stanu w czasie rzeczywistym, ponieważ funkcja LLM generuje argumenty narzędzia, zapewniając optymistyczne aktualizacje interfejsu użytkownika. Migawka stanu końcowego jest emitowana po zakończeniu działania narzędzia.

Implementacja klienta

Pakiet agent_framework_ag_ui zapewnia AGUIChatClient nawiązywanie połączenia z serwerami AG-UI, przenosząc środowisko klienta języka Python do parzystości z platformą .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())

Najważniejsze korzyści

AGUIChatClient zapewnia:

  • Uproszczone połączenie: automatyczna obsługa komunikacji HTTP/SSE
  • Zarządzanie wątkami: wbudowane śledzenie identyfikatorów wątków na potrzeby ciągłości konwersacji
  • Integracja agenta: bezproblemowo współpracuje ze znanym interfejsem Agent API
  • Obsługa stanu: automatyczne analizowanie zdarzeń stanu z serwera
  • Równoważność z platformą .NET: spójne środowisko w różnych językach

Wskazówka

Użyj AGUIChatClient i Agent aby w pełni korzystać z funkcji frameworku agentów, takich jak historia konwersacji, wykonywanie narzędzi i wsparcie dla middleware.

Używanie strategii potwierdzenia

Parametr confirmation_strategy umożliwia dostosowanie komunikatów zatwierdzenia dla domeny:

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

Dostępne strategie:

  • DefaultConfirmationStrategy() - Komunikaty ogólne dla dowolnego agenta
  • RecipeConfirmationStrategy() - Komunikaty specyficzne dla instrukcji
  • DocumentWriterConfirmationStrategy() — Edytowanie komunikatów w dokumencie
  • TaskPlannerConfirmationStrategy() - Komunikaty dotyczące planowania zadań

Można również tworzyć strategie niestandardowe, dziedzicząc z ConfirmationStrategy i implementując wymagane metody.

Przykładowa interakcja

Po uruchomieniu serwera i klienta:

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

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

Wskazówka

Użyj polecenia , :state aby wyświetlić bieżący stan w dowolnym momencie podczas konwersacji.

Aktualizacje stanu predykcyjnego w akcji

W przypadku korzystania z aktualizacji stanu predykcyjnego za pomocą programu, klient otrzymuje zdarzenia, ponieważ usługa LLM generuje argumenty narzędzi w czasie rzeczywistym, zanim narzędzie zostanie uruchomione.

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

Dzięki temu klient może wyświetlać optymistyczne aktualizacje interfejsu użytkownika w czasie rzeczywistym, ponieważ agent myśli, zapewniając natychmiastową opinię użytkownikom.

Stan z interakcją człowieka w pętli

Zarządzanie stanami można połączyć z przepływami pracy zatwierdzania, ustawiając ustawienie 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(),
)

Po włączeniu:

  1. Strumień aktualizacji stanu, gdy agent generuje argumenty narzędzi (aktualizacje predykcyjne za pośrednictwem zdarzeń STATE_DELTA )
  2. Agent prosi o zatwierdzenie przed wykonaniem narzędzia (poprzez zdarzenie FUNCTION_APPROVAL_REQUEST)
  3. Po zatwierdzeniu narzędzie zostaje wykonane, a stan końcowy emitowany jest za pośrednictwem zdarzenia STATE_SNAPSHOT.
  4. W przypadku odrzucenia zmiany stanu predykcyjnego zostaną odrzucone

Zaawansowane wzorce stanu

Stan złożony z wieloma polami

Możesz zarządzać wieloma polami stanu za pomocą różnych narzędzi:

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

Używanie argumentów narzędzi z symbolami wieloznacznymi

Gdy narzędzie zwraca złożone zagnieżdżone dane, użyj polecenia "*", aby przemapować wszystkie argumenty narzędzia na stan:

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

Spowoduje to mapowania całego wywołania narzędzia (wszystkich argumentów) na document pole stanu.

Najlepsze praktyki

Korzystanie z modeli Pydantic

Zdefiniuj modele ustrukturyzowane pod kątem bezpieczeństwa typów:

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

Korzyści:

  • Bezpieczeństwo typów: automatyczna walidacja typów danych
  • Dokumentacja: Opisy pól służą jako dokumentacja
  • Obsługa środowiska IDE: automatyczne uzupełnianie i sprawdzanie typów
  • Serializacja: automatyczna konwersja JSON

Pełne aktualizacje stanu

Zawsze zapisuj pełny stan, a nie tylko różnice:

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

Zapewnia to spójność stanu i odpowiednie aktualizacje predykcyjne.

Dopasowywanie nazw parametrów

Upewnij się, że nazwy parametrów narzędzi są zgodne z tool_argument konfiguracją:

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

Podaj kontekst w instrukcjach

Dołącz jasne instrukcje dotyczące zarządzania stanem:

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

Używanie strategii potwierdzenia

Dostosuj komunikaty zatwierdzenia dla domeny:

from agent_framework_ag_ui import RecipeConfirmationStrategy

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

Dalsze kroki

Znasz już wszystkie podstawowe funkcje AG-UI! Następnie możesz:

Dodatkowe zasoby