Human-in-the-Loop s AG-UI

Tento návod demonstruje, jak implementovat schvalovací pracovní postupy s lidským zásahovým prvkem pomocí AG-UI v .NET. Implementace .NET používá Microsoft.Extensions.AI ApprovalRequiredAIFunction a překládá žádosti o schválení do AG-UI "volání klientských nástrojů", které klient zpracovává a na které reaguje.

Přehled

Model schválení AG-UI jazyka C# funguje takto:

  1. Server: Ovine funkce s ApprovalRequiredAIFunction, aby označil, že vyžadují schválení.
  2. Middleware: Zachytí FunctionApprovalRequestContent z agenta a převede jej na volání klientského nástroje.
  3. Klient: Přijme volání nástroje, zobrazí uživatelské rozhraní schválení a odešle odpověď na schválení jako výsledek nástroje.
  4. Middleware: Rozbalí odpověď schválení a převede ji na FunctionApprovalResponseContent
  5. Agent: Pokračuje v provádění po rozhodnutí uživatele o schválení.

Požadavky

Implementace serveru

Definujte nástroj vyžadující schválení

Vytvořte funkci a zabalte ji pomocí ApprovalRequiredAIFunction:

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

[Description("Send an email to a recipient.")]
static string SendEmail(
    [Description("The email address to send to")] string to,
    [Description("The subject line")] string subject,
    [Description("The email body")] string body)
{
    return $"Email sent to {to} with subject '{subject}'";
}

// Create approval-required tool
#pragma warning disable MEAI001 // Type is for evaluation purposes only
AITool[] tools = [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(SendEmail))];
#pragma warning restore MEAI001

Vytvoření modelů schválení

Definujte modely pro žádost o schválení a odpověď:

using System.Text.Json.Serialization;

public sealed class ApprovalRequest
{
    [JsonPropertyName("approval_id")]
    public required string ApprovalId { get; init; }

    [JsonPropertyName("function_name")]
    public required string FunctionName { get; init; }

    [JsonPropertyName("function_arguments")]
    public JsonElement? FunctionArguments { get; init; }

    [JsonPropertyName("message")]
    public string? Message { get; init; }
}

public sealed class ApprovalResponse
{
    [JsonPropertyName("approval_id")]
    public required string ApprovalId { get; init; }

    [JsonPropertyName("approved")]
    public required bool Approved { get; init; }
}

[JsonSerializable(typeof(ApprovalRequest))]
[JsonSerializable(typeof(ApprovalResponse))]
[JsonSerializable(typeof(Dictionary<string, object?>))]
internal partial class ApprovalJsonContext : JsonSerializerContext
{
}

Implementace schvalovacího middlewaru

Vytvořte middleware, který se překládá mezi typy schválení Microsoft.Extensions.AI a protokolem AG-UI:

Důležité

Po převedení odpovědí na schválení musí být volání nástroje request_approval a jeho výsledek odstraněn z historie zpráv. V opačném případě Azure OpenAI vrátí chybu: "tool_calls musí být následovány zprávami nástroje, které reagují na každý 'tool_call_id'".

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

// Get JsonSerializerOptions from the configured HTTP JSON options
var jsonOptions = app.Services.GetRequiredService<IOptions<Microsoft.AspNetCore.Http.Json.JsonOptions>>().Value;

var agent = baseAgent
    .AsBuilder()
    .Use(runFunc: null, runStreamingFunc: (messages, session, options, innerAgent, cancellationToken) =>
        HandleApprovalRequestsMiddleware(
            messages,
            session,
            options,
            innerAgent,
            jsonOptions.SerializerOptions,
            cancellationToken))
    .Build();

static async IAsyncEnumerable<AgentResponseUpdate> HandleApprovalRequestsMiddleware(
    IEnumerable<ChatMessage> messages,
    AgentSession? session,
    AgentRunOptions? options,
    AIAgent innerAgent,
    JsonSerializerOptions jsonSerializerOptions,
    [EnumeratorCancellation] CancellationToken cancellationToken)
{
    // Process messages: Convert approval responses back to agent format
    var modifiedMessages = ConvertApprovalResponsesToFunctionApprovals(messages, jsonSerializerOptions);

    // Invoke inner agent
    await foreach (var update in innerAgent.RunStreamingAsync(
        modifiedMessages, session, options, cancellationToken))
    {
        // Process updates: Convert approval requests to client tool calls
        await foreach (var processedUpdate in ConvertFunctionApprovalsToToolCalls(update, jsonSerializerOptions))
        {
            yield return processedUpdate;
        }
    }

    // Local function: Convert approval responses from client back to FunctionApprovalResponseContent
    static IEnumerable<ChatMessage> ConvertApprovalResponsesToFunctionApprovals(
        IEnumerable<ChatMessage> messages,
        JsonSerializerOptions jsonSerializerOptions)
    {
        // Look for "request_approval" tool calls and their matching results
        Dictionary<string, FunctionCallContent> approvalToolCalls = [];
        FunctionResultContent? approvalResult = null;

        foreach (var message in messages)
        {
            foreach (var content in message.Contents)
            {
                if (content is FunctionCallContent { Name: "request_approval" } toolCall)
                {
                    approvalToolCalls[toolCall.CallId] = toolCall;
                }
                else if (content is FunctionResultContent result && approvalToolCalls.ContainsKey(result.CallId))
                {
                    approvalResult = result;
                }
            }
        }

        // If no approval response found, return messages unchanged
        if (approvalResult == null)
        {
            return messages;
        }

        // Deserialize the approval response
        if ((approvalResult.Result as JsonElement?)?.Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(ApprovalResponse))) is not ApprovalResponse response)
        {
            return messages;
        }

        // Extract the original function call details from the approval request
        var originalToolCall = approvalToolCalls[approvalResult.CallId];

        if (originalToolCall.Arguments?.TryGetValue("request", out JsonElement request) != true ||
            request.Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(ApprovalRequest))) is not ApprovalRequest approvalRequest)
        {
            return messages;
        }

        // Deserialize the function arguments from JsonElement
        var functionArguments = approvalRequest.FunctionArguments is { } args
            ? (Dictionary<string, object?>?)args.Deserialize(
                jsonSerializerOptions.GetTypeInfo(typeof(Dictionary<string, object?>)))
            : null;

        var originalFunctionCall = new FunctionCallContent(
            callId: response.ApprovalId,
            name: approvalRequest.FunctionName,
            arguments: functionArguments);

        var functionApprovalResponse = new FunctionApprovalResponseContent(
            response.ApprovalId,
            response.Approved,
            originalFunctionCall);

        // Replace/remove the approval-related messages
        List<ChatMessage> newMessages = [];
        foreach (var message in messages)
        {
            bool hasApprovalResult = false;
            bool hasApprovalRequest = false;

            foreach (var content in message.Contents)
            {
                if (content is FunctionResultContent { CallId: var callId } && callId == approvalResult.CallId)
                {
                    hasApprovalResult = true;
                    break;
                }
                if (content is FunctionCallContent { Name: "request_approval", CallId: var reqCallId } && reqCallId == approvalResult.CallId)
                {
                    hasApprovalRequest = true;
                    break;
                }
            }

            if (hasApprovalResult)
            {
                // Replace tool result with approval response
                newMessages.Add(new ChatMessage(ChatRole.User, [functionApprovalResponse]));
            }
            else if (hasApprovalRequest)
            {
                // Skip the request_approval tool call message
                continue;
            }
            else
            {
                newMessages.Add(message);
            }
        }

        return newMessages;
    }

    // Local function: Convert FunctionApprovalRequestContent to client tool calls
    static async IAsyncEnumerable<AgentResponseUpdate> ConvertFunctionApprovalsToToolCalls(
        AgentResponseUpdate update,
        JsonSerializerOptions jsonSerializerOptions)
    {
        // Check if this update contains a FunctionApprovalRequestContent
        FunctionApprovalRequestContent? approvalRequestContent = null;
        foreach (var content in update.Contents)
        {
            if (content is FunctionApprovalRequestContent request)
            {
                approvalRequestContent = request;
                break;
            }
        }

        // If no approval request, yield the update unchanged
        if (approvalRequestContent == null)
        {
            yield return update;
            yield break;
        }

        // Convert the approval request to a "client tool call"
        var functionCall = approvalRequestContent.FunctionCall;
        var approvalId = approvalRequestContent.Id;

        // Serialize the function arguments as JsonElement
        var argsElement = functionCall.Arguments?.Count > 0
            ? JsonSerializer.SerializeToElement(functionCall.Arguments, jsonSerializerOptions.GetTypeInfo(typeof(IDictionary<string, object?>)))
            : (JsonElement?)null;

        var approvalData = new ApprovalRequest
        {
            ApprovalId = approvalId,
            FunctionName = functionCall.Name,
            FunctionArguments = argsElement,
            Message = $"Approve execution of '{functionCall.Name}'?"
        };

        var approvalJson = JsonSerializer.Serialize(approvalData, jsonSerializerOptions.GetTypeInfo(typeof(ApprovalRequest)));

        // Yield a tool call update that represents the approval request
        yield return new AgentResponseUpdate(ChatRole.Assistant, [
            new FunctionCallContent(
                callId: approvalId,
                name: "request_approval",
                arguments: new Dictionary<string, object?> { ["request"] = approvalJson })
        ]);
    }
}

Implementace klienta

Implementace klientského middlewaru

Klient vyžaduje obousměrný middleware , který zpracovává obojí:

  1. Příchozí: Převod request_approval volání nástroje na FunctionApprovalRequestContent
  2. Odchozí: Převod FunctionApprovalResponseContent zpět na výsledky nástrojů

Důležité

AdditionalProperties Pomocí AIContent objektů můžete sledovat korelaci mezi žádostmi o schválení a odpověďmi a vyhnout se slovníkům externího stavu.

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

// Get JsonSerializerOptions from the client
var jsonSerializerOptions = JsonSerializerOptions.Default;

#pragma warning disable MEAI001 // Type is for evaluation purposes only
// Wrap the agent with approval middleware
var wrappedAgent = agent
    .AsBuilder()
    .Use(runFunc: null, runStreamingFunc: (messages, session, options, innerAgent, cancellationToken) =>
        HandleApprovalRequestsClientMiddleware(
            messages,
            session,
            options,
            innerAgent,
            jsonSerializerOptions,
            cancellationToken))
    .Build();

static async IAsyncEnumerable<AgentResponseUpdate> HandleApprovalRequestsClientMiddleware(
    IEnumerable<ChatMessage> messages,
    AgentSession? session,
    AgentRunOptions? options,
    AIAgent innerAgent,
    JsonSerializerOptions jsonSerializerOptions,
    [EnumeratorCancellation] CancellationToken cancellationToken)
{
    // Process messages: Convert approval responses back to tool results
    var processedMessages = ConvertApprovalResponsesToToolResults(messages, jsonSerializerOptions);

    // Invoke inner agent
    await foreach (var update in innerAgent.RunStreamingAsync(processedMessages, session, options, cancellationToken))
    {
        // Process updates: Convert tool calls to approval requests
        await foreach (var processedUpdate in ConvertToolCallsToApprovalRequests(update, jsonSerializerOptions))
        {
            yield return processedUpdate;
        }
    }

    // Local function: Convert FunctionApprovalResponseContent back to tool results
    static IEnumerable<ChatMessage> ConvertApprovalResponsesToToolResults(
        IEnumerable<ChatMessage> messages,
        JsonSerializerOptions jsonSerializerOptions)
    {
        List<ChatMessage> processedMessages = [];

        foreach (var message in messages)
        {
            List<AIContent> convertedContents = [];
            bool hasApprovalResponse = false;

            foreach (var content in message.Contents)
            {
                if (content is FunctionApprovalResponseContent approvalResponse)
                {
                    hasApprovalResponse = true;

                    // Get the original request_approval CallId from AdditionalProperties
                    if (approvalResponse.AdditionalProperties?.TryGetValue("request_approval_call_id", out string? requestApprovalCallId) == true)
                    {
                        var response = new ApprovalResponse
                        {
                            ApprovalId = approvalResponse.Id,
                            Approved = approvalResponse.Approved
                        };

                        var responseJson = JsonSerializer.SerializeToElement(response, jsonSerializerOptions.GetTypeInfo(typeof(ApprovalResponse)));

                        var toolResult = new FunctionResultContent(
                            callId: requestApprovalCallId,
                            result: responseJson);

                        convertedContents.Add(toolResult);
                    }
                }
                else
                {
                    convertedContents.Add(content);
                }
            }

            if (hasApprovalResponse && convertedContents.Count > 0)
            {
                processedMessages.Add(new ChatMessage(ChatRole.Tool, convertedContents));
            }
            else
            {
                processedMessages.Add(message);
            }
        }

        return processedMessages;
    }

    // Local function: Convert request_approval tool calls to FunctionApprovalRequestContent
    static async IAsyncEnumerable<AgentResponseUpdate> ConvertToolCallsToApprovalRequests(
        AgentResponseUpdate update,
        JsonSerializerOptions jsonSerializerOptions)
    {
        FunctionCallContent? approvalToolCall = null;
        foreach (var content in update.Contents)
        {
            if (content is FunctionCallContent { Name: "request_approval" } toolCall)
            {
                approvalToolCall = toolCall;
                break;
            }
        }

        if (approvalToolCall == null)
        {
            yield return update;
            yield break;
        }

        if (approvalToolCall.Arguments?.TryGetValue("request", out JsonElement request) != true ||
            request.Deserialize(jsonSerializerOptions.GetTypeInfo(typeof(ApprovalRequest))) is not ApprovalRequest approvalRequest)
        {
            yield return update;
            yield break;
        }

        var functionArguments = approvalRequest.FunctionArguments is { } args
            ? (Dictionary<string, object?>?)args.Deserialize(
                jsonSerializerOptions.GetTypeInfo(typeof(Dictionary<string, object?>)))
            : null;

        var originalFunctionCall = new FunctionCallContent(
            callId: approvalRequest.ApprovalId,
            name: approvalRequest.FunctionName,
            arguments: functionArguments);

        // Yield the original tool call first (for message history)
        yield return new AgentResponseUpdate(ChatRole.Assistant, [approvalToolCall]);

        // Create approval request with CallId stored in AdditionalProperties
        var approvalRequestContent = new FunctionApprovalRequestContent(
            approvalRequest.ApprovalId,
            originalFunctionCall);

        // Store the request_approval CallId in AdditionalProperties for later retrieval
        approvalRequestContent.AdditionalProperties ??= new Dictionary<string, object?>();
        approvalRequestContent.AdditionalProperties["request_approval_call_id"] = approvalToolCall.CallId;

        yield return new AgentResponseUpdate(ChatRole.Assistant, [approvalRequestContent]);
    }
}
#pragma warning restore MEAI001

Zpracování žádostí o schválení a odesílání odpovědí

Zpracovávající kód zpracovává žádosti o schválení a automaticky pokračuje, dokud již nejsou potřeba další schválení.

Zpracování žádostí o schválení a odesílání odpovědí

Kód, který využívá, zpracovává žádosti o schválení. Při přijetí FunctionApprovalRequestContent uložte CallId schválení žádosti do AdditionalProperties odpovědi.

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

#pragma warning disable MEAI001 // Type is for evaluation purposes only
List<AIContent> approvalResponses = [];
List<FunctionCallContent> approvalToolCalls = [];

do
{
    approvalResponses.Clear();
    approvalToolCalls.Clear();

    await foreach (AgentResponseUpdate update in wrappedAgent.RunStreamingAsync(
        messages, session, cancellationToken: cancellationToken))
    {
        foreach (AIContent content in update.Contents)
        {
            if (content is FunctionApprovalRequestContent approvalRequest)
            {
                DisplayApprovalRequest(approvalRequest);

                // Get user approval
                Console.Write($"\nApprove '{approvalRequest.FunctionCall.Name}'? (yes/no): ");
                string? userInput = Console.ReadLine();
                bool approved = userInput?.ToUpperInvariant() is "YES" or "Y";

                // Create approval response and preserve the request_approval CallId
                var approvalResponse = approvalRequest.CreateResponse(approved);

                // Copy AdditionalProperties to preserve the request_approval_call_id
                if (approvalRequest.AdditionalProperties != null)
                {
                    approvalResponse.AdditionalProperties ??= new Dictionary<string, object?>();
                    foreach (var kvp in approvalRequest.AdditionalProperties)
                    {
                        approvalResponse.AdditionalProperties[kvp.Key] = kvp.Value;
                    }
                }

                approvalResponses.Add(approvalResponse);
            }
            else if (content is FunctionCallContent { Name: "request_approval" } requestApprovalCall)
            {
                // Track the original request_approval tool call
                approvalToolCalls.Add(requestApprovalCall);
            }
            else if (content is TextContent textContent)
            {
                Console.Write(textContent.Text);
            }
        }
    }

    // Add both messages in correct order
    if (approvalResponses.Count > 0 && approvalToolCalls.Count > 0)
    {
        messages.Add(new ChatMessage(ChatRole.Assistant, approvalToolCalls.ToArray()));
        messages.Add(new ChatMessage(ChatRole.User, approvalResponses.ToArray()));
    }
}
while (approvalResponses.Count > 0);
#pragma warning restore MEAI001

static void DisplayApprovalRequest(FunctionApprovalRequestContent approvalRequest)
{
    Console.WriteLine();
    Console.WriteLine("============================================================");
    Console.WriteLine("APPROVAL REQUIRED");
    Console.WriteLine("============================================================");
    Console.WriteLine($"Function: {approvalRequest.FunctionCall.Name}");

    if (approvalRequest.FunctionCall.Arguments != null)
    {
        Console.WriteLine("Arguments:");
        foreach (var arg in approvalRequest.FunctionCall.Arguments)
        {
            Console.WriteLine($"  {arg.Key} = {arg.Value}");
        }
    }

    Console.WriteLine("============================================================");
}

Příklad interakce

User (:q or quit to exit): Send an email to user@example.com about the meeting

[Run Started - Thread: thread_abc123, Run: run_xyz789]

============================================================
APPROVAL REQUIRED
============================================================

Function: SendEmail
Arguments: {"to":"user@example.com","subject":"Meeting","body":"..."}
Message: Approve execution of 'SendEmail'?

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

[Waiting for approval to execute SendEmail...]
[Run Finished - Thread: thread_abc123]

Approve this action? (yes/no): yes

[Sending approval response: APPROVED]

[Run Resumed - Thread: thread_abc123]
Email sent to user@example.com with subject 'Meeting'
[Run Finished]

Klíčové koncepty

Model klientského nástroje

Implementace jazyka C# používá vzor volání klientského nástroje:

  • Žádost o schválení → Volání nástroje s názvem "request_approval" s podrobnostmi o schválení
  • Odpověď na schválení → Výsledek nástroje obsahující rozhodnutí uživatele
  • Middleware → Překládá mezi typy Microsoft.Extensions.AI a protokolem AG-UI

To umožňuje standardnímu ApprovalRequiredAIFunction vzoru pracovat na hranicích HTTP+SSE a současně udržovat konzistenci s modelem schválení architektury agenta.

Vzor obousměrného middlewaru

Serverový i klientský middleware se řídí konzistentním třístupňovým vzorem:

  1. Zpracování zpráv: Transformace příchozích zpráv (odpovědi na schválení → FunctionApprovalResponseContent nebo výsledky nástroje)
  2. Vyvolání vnitřního agenta: Volání vnitřního agenta se zpracovanými zprávami
  3. Aktualizace procesů: Transformace odchozích aktualizací (FunctionApprovalRequestContent → volání nástroje nebo naopak)

Sledování stavu pomocí additionalProperties

Místo externích slovníků se implementace používá AdditionalProperties pro AIContent objekty ke sledování metadat:

  • Klient: Klient ukládá request_approval_call_id do FunctionApprovalRequestContent.AdditionalProperties
  • Zachování odpovědí: Kopie AdditionalProperties z požadavku na odpověď za účelem zachování korelace
  • Převod: Používá uložené ID volání k vytvoření správné korelace. FunctionResultContent

Díky tomu jsou všechna data korelace v samotných objektech obsahu, abyste se vyhnuli nutnosti správy externího stavu.

Server-Side vyčištění zpráv

Serverové middleware musí po zpracování odebrat zprávy protokolu schvalování.

  • Problem: Azure OpenAI vyžaduje, aby všechna volání nástrojů měla odpovídající výsledky nástroje.
  • Řešení: Po převodu schvalovacích odpovědí odeberte volání nástroje request_approval i jeho zprávu o výsledku.
  • Důvod: Zabrání chybám "tool_calls musí být následovány zprávami nástroje"

Další kroky

V tomto kurzu se dozvíte, jak implementovat pracovní postupy human-in-the-loop s AG-UI, kde uživatelé musí schválit spuštění nástrojů před jejich provedením. To je nezbytné pro citlivé operace, jako jsou finanční transakce, úpravy dat nebo akce, které mají významné důsledky.

Požadavky

Než začnete, ujistěte se, že jste dokončili kurz vykreslování back-endových nástrojů a pochopili:

  • Jak vytvořit funkční nástroje
  • Jak AG-UI streamuje události nástroje
  • Základní nastavení serveru a klienta

Co je human-in-the-loop?

HitL (Human-in-the-Loop) je vzor, ve kterém agent před provedením určitých operací požaduje schválení uživatele. S AG-UI:

  • Agent generuje volání nástrojů obvyklým způsobem.
  • Místo okamžitého spuštění server odešle klientovi žádosti o schválení.
  • Klient zobrazí požadavek a vyzve uživatele.
  • Uživatel akci schválí nebo odmítne.
  • Server obdrží odpověď a bude odpovídajícím způsobem pokračovat.

Výhody

  • Bezpečnost: Zabránění provedení nezamýšlených akcí
  • Transparentnost: Uživatelé přesně vidí, co chce agent dělat.
  • Řízení: Uživatelé mají konečné slovo o citlivých operacích.
  • Dodržování předpisů: Splnění zákonných požadavků pro lidský dohled

Označovací nástroje pro schválení

Pokud chcete vyžadovat schválení nástroje, použijte approval_mode parametr v dekorátoru @tool :

from agent_framework import tool
from typing import Annotated
from pydantic import Field


@tool(approval_mode="always_require")
def send_email(
    to: Annotated[str, Field(description="Email recipient address")],
    subject: Annotated[str, Field(description="Email subject line")],
    body: Annotated[str, Field(description="Email body content")],
) -> str:
    """Send an email to the specified recipient."""
    # Send email logic here
    return f"Email sent to {to} with subject '{subject}'"


@tool(approval_mode="always_require")
def delete_file(
    filepath: Annotated[str, Field(description="Path to the file to delete")],
) -> str:
    """Delete a file from the filesystem."""
    # Delete file logic here
    return f"File {filepath} has been deleted"

Režimy schválení

  • always_require: Před provedením vždy požádejte o schválení.
  • never_require: Nikdy nepožadujte schválení (výchozí chování)
  • conditional: Požádat o schválení podle určitých podmínek (vlastní logika)

Vytvoření serveru s implementací Human-in-the-Loop

Tady je kompletní implementace serveru s nástroji potřebnými ke schválení:

"""AG-UI server with human-in-the-loop."""

import os
from typing import Annotated

from agent_framework import Agent, tool
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
from pydantic import Field


# Tools that require approval
@tool(approval_mode="always_require")
def transfer_money(
    from_account: Annotated[str, Field(description="Source account number")],
    to_account: Annotated[str, Field(description="Destination account number")],
    amount: Annotated[float, Field(description="Amount to transfer")],
    currency: Annotated[str, Field(description="Currency code")] = "USD",
) -> str:
    """Transfer money between accounts."""
    return f"Transferred {amount} {currency} from {from_account} to {to_account}"


@tool(approval_mode="always_require")
def cancel_subscription(
    subscription_id: Annotated[str, Field(description="Subscription identifier")],
) -> str:
    """Cancel a subscription."""
    return f"Subscription {subscription_id} has been cancelled"


# Regular tools (no approval required)
@tool
def check_balance(
    account: Annotated[str, Field(description="Account number")],
) -> str:
    """Check account balance."""
    # Simulated balance check
    return f"Account {account} balance: $5,432.10 USD"


# Read required configuration
endpoint = os.environ.get("AZURE_OPENAI_ENDPOINT")
deployment_name = os.environ.get("AZURE_OPENAI_CHAT_COMPLETION_MODEL")

if not endpoint:
    raise ValueError("AZURE_OPENAI_ENDPOINT environment variable is required")
if not deployment_name:
    raise ValueError("AZURE_OPENAI_CHAT_COMPLETION_MODEL environment variable is required")

chat_client = OpenAIChatCompletionClient(
    model=deployment_name,
    azure_endpoint=endpoint,
    api_version=os.getenv("AZURE_OPENAI_API_VERSION"),
    credential=AzureCliCredential(),
)

# Create agent with tools
agent = Agent(
    name="BankingAssistant",
    instructions="You are a banking assistant. Help users with their banking needs. Always confirm details before performing transfers.",
    client=chat_client,
    tools=[transfer_money, cancel_subscription, check_balance],
)

# Wrap agent to enable human-in-the-loop
wrapped_agent = AgentFrameworkAgent(
    agent=agent,
    require_confirmation=True,  # Enable human-in-the-loop
)

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

if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app, host="127.0.0.1", port=8888)

Klíčové koncepty

  • AgentFrameworkAgent wrapper: Povoluje funkce protokolu AG-UI, jako je human-in-the-loop
  • require_confirmation=True: Aktivuje pracovní postup schválení pro označené nástroje.
  • Řízení na úrovni nástrojů: Schválení budou požadovat pouze nástroje označené pomocí approval_mode="always_require"

Porozumění schvalovacím událostem

Když nástroj vyžaduje schválení, klient obdrží tyto události:

Událost žádosti o schválení

{
    "type": "APPROVAL_REQUEST",
    "approvalId": "approval_abc123",
    "steps": [
        {
            "toolCallId": "call_xyz789",
            "toolCallName": "transfer_money",
            "arguments": {
                "from_account": "1234567890",
                "to_account": "0987654321",
                "amount": 500.00,
                "currency": "USD"
            }
        }
    ],
    "message": "Do you approve the following actions?"
}

Formát odpovědi na schválení

Klient musí odeslat odpověď na schválení:

# Approve
{
    "type": "APPROVAL_RESPONSE",
    "approvalId": "approval_abc123",
    "approved": True
}

# Reject
{
    "type": "APPROVAL_RESPONSE",
    "approvalId": "approval_abc123",
    "approved": False
}

Klient s podporou schválení

Tady je klient používající AGUIChatClient, který zpracovává žádosti o schválení.

"""AG-UI client with human-in-the-loop support."""

import asyncio
import os

from agent_framework import Agent, ToolCallContent, ToolResultContent
from agent_framework_ag_ui import AGUIChatClient


def display_approval_request(update) -> None:
    """Display approval request details to the user."""
    print("\n\033[93m" + "=" * 60 + "\033[0m")
    print("\033[93mAPPROVAL REQUIRED\033[0m")
    print("\033[93m" + "=" * 60 + "\033[0m")

    # Display tool call details from update contents
    for i, content in enumerate(update.contents, 1):
        if isinstance(content, ToolCallContent):
            print(f"\nAction {i}:")
            print(f"  Tool: \033[95m{content.name}\033[0m")
            print(f"  Arguments:")
            for key, value in (content.arguments or {}).items():
                print(f"    {key}: {value}")

    print("\n\033[93m" + "=" * 60 + "\033[0m")


async def main():
    """Main client loop with approval handling."""
    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)

    # Create agent with the chat client
    agent = Agent(
        name="ClientAgent",
        client=chat_client,
        instructions="You are a helpful assistant.",
    )

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

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

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

            print("\nAssistant: ", end="", flush=True)
            pending_approval_update = None

            async for update in agent.run(message, session=thread, stream=True):
                # Check if this is an approval request
                # (Approval requests are detected by specific metadata or content markers)
                if update.additional_properties and update.additional_properties.get("requires_approval"):
                    pending_approval_update = update
                    display_approval_request(update)
                    break  # Exit the loop to handle approval

                elif event_type == "RUN_FINISHED":
                    print(f"\n\033[92m[Run Finished]\033[0m")

                elif event_type == "RUN_ERROR":
                    error_msg = event.get("message", "Unknown error")
                    print(f"\n\033[91m[Error: {error_msg}]\033[0m")

            # Handle approval request
            if pending_approval:
                approval_id = pending_approval.get("approvalId")
                user_choice = input("\nApprove this action? (yes/no): ").strip().lower()
                approved = user_choice in ("yes", "y")

                print(f"\n\033[93m[Sending approval response: {approved}]\033[0m\n")

                async for event in client.send_approval_response(approval_id, approved):
                    event_type = event.get("type", "")

                    if event_type == "TEXT_MESSAGE_CONTENT":
                        print(f"\033[96m{event.get('delta', '')}\033[0m", end="", flush=True)

                    elif event_type == "TOOL_CALL_RESULT":
                        content = event.get("content", "")
                        print(f"\033[94m[Tool Result: {content}]\033[0m")

                    elif event_type == "RUN_FINISHED":
                        print(f"\n\033[92m[Run Finished]\033[0m")

                    elif event_type == "RUN_ERROR":
                        error_msg = event.get("message", "Unknown error")
                        print(f"\n\033[91m[Error: {error_msg}]\033[0m")

            print()

    except KeyboardInterrupt:
        print("\n\nExiting...")
    except Exception as e:
        print(f"\n\033[91mError: {e}\033[0m")


if __name__ == "__main__":
    asyncio.run(main())

Příklad interakce

Se spuštěným serverem a klientem:

User (:q or quit to exit): Transfer $500 from account 1234567890 to account 0987654321

[Run Started]
============================================================
APPROVAL REQUIRED
============================================================

Action 1:
  Tool: transfer_money
  Arguments:
    from_account: 1234567890
    to_account: 0987654321
    amount: 500.0
    currency: USD

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

Approve this action? (yes/no): yes

[Sending approval response: True]

[Tool Result: Transferred 500.0 USD from 1234567890 to 0987654321]
The transfer of $500 from account 1234567890 to account 0987654321 has been completed successfully.
[Run Finished]

Pokud uživatel odmítne:

Approve this action? (yes/no): no

[Sending approval response: False]

I understand. The transfer has been cancelled and no money was moved.
[Run Finished]

Vlastní potvrzovací zprávy

Zprávy o schválení můžete přizpůsobit tak, že poskytnete vlastní strategii potvrzení:

from typing import Any
from agent_framework_ag_ui import AgentFrameworkAgent, ConfirmationStrategy


class BankingConfirmationStrategy(ConfirmationStrategy):
    """Custom confirmation messages for banking operations."""

    def on_approval_accepted(self, steps: list[dict[str, Any]]) -> str:
        """Message when user approves the action."""
        tool_name = steps[0].get("toolCallName", "action")
        return f"Thank you for confirming. Proceeding with {tool_name}..."

    def on_approval_rejected(self, steps: list[dict[str, Any]]) -> str:
        """Message when user rejects the action."""
        return "Action cancelled. No changes have been made to your account."

    def on_state_confirmed(self) -> str:
        """Message when state changes are confirmed."""
        return "Changes confirmed and applied."

    def on_state_rejected(self) -> str:
        """Message when state changes are rejected."""
        return "Changes discarded."


# Use custom strategy
wrapped_agent = AgentFrameworkAgent(
    agent=agent,
    require_confirmation=True,
    confirmation_strategy=BankingConfirmationStrategy(),
)

Osvědčené postupy

Vymazat popisy nástrojů

Zadejte podrobné popisy, aby uživatelé pochopili, co schvalují:

@tool(approval_mode="always_require")
def delete_database(
    database_name: Annotated[str, Field(description="Name of the database to permanently delete")],
) -> str:
    """
    Permanently delete a database and all its contents.

    WARNING: This action cannot be undone. All data in the database will be lost.
    Use with extreme caution.
    """
    # Implementation
    pass

Granulární schválení

Místo dávkování požádejte o schválení jednotlivých citlivých akcí:

# Good: Individual approval per transfer
@tool(approval_mode="always_require")
def transfer_money(...): pass

# Avoid: Batching multiple sensitive operations
# Users should approve each operation separately

Informativní argumenty

Použijte popisné názvy parametrů a zadejte kontext:

@tool(approval_mode="always_require")
def purchase_item(
    item_name: Annotated[str, Field(description="Name of the item to purchase")],
    quantity: Annotated[int, Field(description="Number of items to purchase")],
    price_per_item: Annotated[float, Field(description="Price per item in USD")],
    total_cost: Annotated[float, Field(description="Total cost including tax and shipping")],
) -> str:
    """Purchase items from the store."""
    pass

Zpracování časového limitu

Nastavte vhodné časové limity pro žádosti o schválení:

# Client side
async with httpx.AsyncClient(timeout=120.0) as client:  # 2 minutes for user to respond
    # Handle approval
    pass

Selektivní schválení

Nástroje, které vyžadují schválení, můžete kombinovat s nástroji, které nemají:

# No approval needed for read-only operations
@tool
def get_account_balance(...): pass

@tool
def list_transactions(...): pass

# Approval required for write operations
@tool(approval_mode="always_require")
def transfer_funds(...): pass

@tool(approval_mode="always_require")
def close_account(...): pass

Další kroky

Další zdroje