Notatka
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
W tym samouczku pokazano, jak zaimplementować przepływy pracy zatwierdzania z udziałem człowieka za pomocą AG-UI w środowisku .NET. Implementacja .NET używa Microsoft.Extensions.AI ApprovalRequiredAIFunction i tłumaczy żądania zatwierdzenia na "wywołania narzędzi klienckich" AG-UI, które klient obsługuje i na które reaguje.
Przegląd
Wzorzec zatwierdzania AG-UI języka C# działa w następujący sposób:
-
Serwer: Opakowuje funkcje
ApprovalRequiredAIFunction, aby oznaczyć je jako wymagające zatwierdzenia - Oprogramowanie pośredniczące: przechwytuje dane od agenta i przekształca je na wywołanie narzędzia klienckiego
- Klient: odbiera żądanie narzędzia, wyświetla interfejs zatwierdzenia i wysyła odpowiedź o zatwierdzeniu jako wynik narzędzia
-
Oprogramowanie pośredniczące: odpakowuje odpowiedź zatwierdzenia i konwertuje ją na
FunctionApprovalResponseContent - Agent: Kontynuuje wykonywanie na podstawie decyzji użytkownika o zatwierdzeniu
Wymagania wstępne
- zasób Azure OpenAI z wdrożonym modelem
- Zmienne środowiskowe:
AZURE_OPENAI_ENDPOINTAZURE_OPENAI_DEPLOYMENT_NAME
- Omówienie renderowania narzędzi zaplecza
Implementacja serwera
Definicja narzędzia wymagającego zatwierdzenia
Utwórz funkcję i zawij ją za pomocą polecenia 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
Tworzenie modeli zatwierdzania
Zdefiniuj modele dla żądania zatwierdzenia i odpowiedzi:
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
{
}
Implementacja middleware do zatwierdzania
Utwórz oprogramowanie pośredniczące, które tłumaczy między typami zatwierdzania Microsoft.Extensions.AI i protokołem AG-UI:
Ważne
Po przekonwertowaniu odpowiedzi zatwierdzenia, zarówno wywołanie narzędzia request_approval, jak i jego wynik, muszą zostać usunięte z historii komunikatów. W przeciwnym razie Azure interfejs OpenAI zwróci błąd: "tool_calls muszą być wyświetlane komunikaty narzędzi odpowiadające każdemu 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 })
]);
}
}
Implementacja klienta
Implementowanie middleware po stronie klienta
Klient wymaga dwukierunkowego oprogramowania pośredniczącego , które obsługuje oba:
-
Inbound: konwertowanie
request_approvalwywołań narzędzi naFunctionApprovalRequestContent -
Ruch wychodzący: konwertowanie
FunctionApprovalResponseContentz powrotem na wyniki narzędzia
Ważne
Użyj AdditionalProperties w AIContent obiektach, aby śledzić korelację między żądaniami zatwierdzenia i odpowiedziami, unikając słowników stanu zewnętrznego.
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
Obsługa żądań zatwierdzenia i wysyłanie odpowiedzi
Kod konsumpcyjny przetwarza żądania zatwierdzenia i automatycznie kontynuuje, aż nie będą potrzebne żadne kolejne zatwierdzenia.
Obsługa żądań zatwierdzenia i wysyłanie odpowiedzi
Kod konsumujący przetwarza żądania zatwierdzenia. Podczas odbierania żądania FunctionApprovalRequestContent, zapisz identyfikator CallId request_approval w polu AdditionalProperties odpowiedzi.
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("============================================================");
}
Przykładowa interakcja
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]
Kluczowe pojęcia
Wzorzec narzędzia klienta
Implementacja języka C# używa wzorca "wywołania narzędzia klienckiego":
-
Żądanie zatwierdzenia → Wywołanie narzędzia o nazwie ze
"request_approval"szczegółami zatwierdzenia - Wynik narzędzia → odpowiedzi na zatwierdzenie zawierający decyzję użytkownika
- Middleware → Tłumaczy między typami Microsoft.Extensions.AI a protokołem AG-UI
Dzięki temu wzorzec standardowy ApprovalRequiredAIFunction może działać w granicach PROTOKOŁU HTTP+SSE przy zachowaniu spójności z modelem zatwierdzania platformy agenta.
Wzorzec oprogramowania pośredniczącego dwukierunkowego
Oprogramowanie pośredniczące serwera i klienta jest zgodne ze spójnym wzorcem trzyetapowym:
- Przetwarzanie komunikatów: przekształcanie komunikatów przychodzących (odpowiedzi zatwierdzenia → FunctionApprovalResponseContent lub wyniki narzędzia)
- Wywołaj agenta wewnętrznego: wywołaj agenta wewnętrznego z przetworzonymi komunikatami
- Aktualizacje procesów: przekształcanie aktualizacji wychodzących (FunctionApprovalRequestContent → wywołania narzędzi lub odwrotnie)
Śledzenie stanu za pomocą dodatkowych właściwości
Zamiast zewnętrznych słowników, implementacja używa AdditionalProperties na obiektach AIContent do śledzenia metadanych.
-
Klient: przechowuje
request_approval_call_idwFunctionApprovalRequestContent.AdditionalProperties -
Zachowywanie odpowiedzi: kopie
AdditionalPropertiesz żądania na odpowiedź w celu zachowania korelacji -
Konwersja: używa przechowywanego identyfikatora CallId do poprawnego skorelowania
FunctionResultContent
Dzięki temu wszystkie dane korelacji w obiektach zawartości są przechowywane bez konieczności zarządzania stanem zewnętrznym.
Czyszczenie komunikatów po stronie serwera
Oprogramowanie pośredniczące serwera musi usunąć komunikaty protokołu zatwierdzania po przetworzeniu:
- Problem: Azure OpenAI wymaga, aby wszystkie wywołania narzędzi miały zgodne wyniki narzędzia
- Rozwiązanie: Po dokonaniu konwersji odpowiedzi dotyczących zatwierdzeń usuń zarówno wywołanie narzędzia, jak i jego komunikat wynikowy.
- Przyczyna: Zapobiega błędom "tool_calls muszą być poprzedzone komunikatami narzędzi"
Następne kroki
W tym samouczku pokazano, jak zaimplementować procesy robocze z udziałem człowieka za pomocą AG-UI, gdzie użytkownicy muszą zatwierdzać uruchamianie narzędzi przed ich wykonaniem. Jest to niezbędne w przypadku operacji poufnych, takich jak transakcje finansowe, modyfikacje danych lub działania, które mają poważne konsekwencje.
Wymagania wstępne
Przed rozpoczęciem upewnij się, że ukończono samouczek renderowania narzędzi backendowych i zapoznaj się z tematem:
- Jak tworzyć narzędzia funkcji
- Jak AG-UI przesyła strumieniowo wydarzenia narzędzi
- Konfiguracja serwera podstawowego i klienta
Co to jest human-in-the-loop?
Human-in-the-Loop (HITL) to wzorzec, w którym agent żąda zatwierdzenia przez użytkownika przed wykonaniem określonych operacji. Z AG-UI:
- Agent generuje wywołania narzędzi w zwykły sposób
- Zamiast wykonywać natychmiast, serwer wysyła żądania zatwierdzenia do klienta
- Klient wyświetla żądanie i monituje użytkownika
- Użytkownik zatwierdza lub odrzuca akcję
- Serwer otrzymuje odpowiedź i postępuje odpowiednio
Korzyści
- Bezpieczeństwo: uniemożliwia wykonywanie niezamierzonych akcji
- Przezroczystość: użytkownicy widzą dokładnie to, co agent chce zrobić
- Kontrola: Użytkownicy mają ostateczne informacje o operacjach poufnych
- Zgodność: spełnianie wymagań regulacyjnych dotyczących nadzoru ludzkiego
Wyznaczanie narzędzi zatwierdzania
Aby wymagać zatwierdzenia dla narzędzia, użyj parametru approval_mode w dekoratorze @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"
Tryby zatwierdzania
-
always_require: Zawsze żądaj zatwierdzenia przed wykonaniem -
never_require: Nigdy nie żądaj zatwierdzenia (zachowanie domyślne) -
conditional: Żądanie zatwierdzenia na podstawie określonych warunków (logika niestandardowa)
Konfiguracja serwera z udziałem człowieka
Oto kompletna implementacja serwera z narzędziami wymaganymi do zatwierdzenia:
"""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)
Kluczowe pojęcia
-
AgentFrameworkAgentwrapper: włącza funkcje protokołu AG-UI, takie jak człowiek-w-pętli -
require_confirmation=True: Aktywuje przepływ pracy zatwierdzania dla oznaczonych narzędzi -
Kontrola na poziomie narzędzia: tylko narzędzia oznaczone za pomocą
approval_mode="always_require"będą żądać zatwierdzenia
Zrozumienie zdarzeń zatwierdzania
Gdy narzędzie wymaga zatwierdzenia, klient otrzymuje następujące zdarzenia:
Wydarzenie związane z żądaniem zatwierdzenia
{
"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?"
}
Format odpowiedzi akceptacji
Klient musi wysłać odpowiedź na zatwierdzenie:
# Approve
{
"type": "APPROVAL_RESPONSE",
"approvalId": "approval_abc123",
"approved": True
}
# Reject
{
"type": "APPROVAL_RESPONSE",
"approvalId": "approval_abc123",
"approved": False
}
Klient ze wsparciem zatwierdzania
Oto klient, który przetwarza żądania zatwierdzenia przy użyciu AGUIChatClient:
"""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())
Przykładowa interakcja
Po uruchomieniu serwera i klienta:
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]
Jeśli użytkownik odrzuci:
Approve this action? (yes/no): no
[Sending approval response: False]
I understand. The transfer has been cancelled and no money was moved.
[Run Finished]
Niestandardowe komunikaty potwierdzenia
Komunikaty zatwierdzenia można dostosować, podając niestandardową strategię potwierdzenia:
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(),
)
Najlepsze praktyki
Wyczyść opisy narzędzi
Podaj szczegółowe opisy, aby użytkownicy zrozumieli, co zatwierdzają:
@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
Granularne zatwierdzanie
Zażądaj zatwierdzenia poszczególnych akcji poufnych zamiast dzielenia na partie:
# 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
Argumenty informacyjne
Użyj opisowych nazw parametrów i podaj kontekst:
@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
Zarządzanie timeoutem
Ustaw odpowiednie limity czasu dla żądań zatwierdzenia:
# Client side
async with httpx.AsyncClient(timeout=120.0) as client: # 2 minutes for user to respond
# Handle approval
pass
Zatwierdzanie selektywne
Możesz mieszać narzędzia, które wymagają zatwierdzenia z tymi, które nie:
# 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