Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
Tutorial ini menunjukkan cara menerapkan alur kerja persetujuan human-in-the-loop dengan AG-UI dalam .NET. Implementasi .NET menggunakan Microsoft.Extensions.AI ApprovalRequiredAIFunction dan menerjemahkan permintaan persetujuan ke dalam panggilan alat klien AG-UI yang ditangani dan ditanggapi oleh klien.
Gambaran Umum
Pola persetujuan C# AG-UI berfungsi sebagai berikut:
-
Server: Membungkus fungsi dengan
ApprovalRequiredAIFunctionuntuk menandainya sebagai memerlukan persetujuan -
Middleware: Mencegat
FunctionApprovalRequestContentdari agen dan mengonversinya ke panggilan alat klien - Klien: Menerima panggilan alat, menampilkan UI persetujuan, dan mengirim respons persetujuan sebagai hasil alat
-
Middleware: Membongkar respons persetujuan dan mengonversinya menjadi
FunctionApprovalResponseContent - Agen: Melanjutkan eksekusi dengan keputusan persetujuan pengguna
Prasyarat
- Azure sumber daya OpenAI dengan model yang sudah diterapkan
- Variabel lingkungan:
AZURE_OPENAI_ENDPOINTAZURE_OPENAI_DEPLOYMENT_NAME
- Pemahaman tentang Penyajian Alat Backend
Implementasi Server
Tentukan Alat yang Memerlukan Persetujuan
Buat fungsi dan bungkus dengan 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
Membuat Model Persetujuan
Tentukan model untuk permintaan persetujuan dan respons:
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
{
}
Menerapkan Middleware Persetujuan
Buat middleware yang diterjemahkan antara jenis persetujuan Microsoft.Extensions.AI dan protokol AG-UI:
Penting
Setelah mengonversi respons persetujuan, baik panggilan alat request_approval maupun hasilnya harus dihapus dari riwayat pesan. Jika tidak, Azure OpenAI akan mengembalikan kesalahan: "tool_calls harus diikuti oleh pesan alat yang merespons setiap '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 })
]);
}
}
Implementasi Klien
Mengimplementasikan middleware di sisi klien
Klien memerlukan middleware dua arah yang menangani keduanya:
-
Proses Masuk: Mengubah
request_approvalpanggilan alat menjadiFunctionApprovalRequestContent -
Pengiriman: Mengonversi
FunctionApprovalResponseContentkembali ke hasil pemrosesan alat
Penting
Gunakan AdditionalProperties pada AIContent objek untuk melacak korelasi antara permintaan persetujuan dan respons, menghindari kamus status eksternal.
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
Menangani Permintaan Persetujuan dan Mengirim Respons
Kode yang mengonsumsi memproses permintaan persetujuan dan melanjutkan secara otomatis hingga tidak diperlukan persetujuan lagi.
Menangani Permintaan Persetujuan dan Mengirim Respons
Kode yang menggunakan memproses permintaan persetujuan. Saat menerima FunctionApprovalRequestContent, simpan CallId dari request_approval ke dalam AdditionalProperties respons:
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("============================================================");
}
Contoh Interaksi
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]
Konsep utama
Pola Alat Klien
Implementasi C# menggunakan pola "panggilan alat klien":
-
Permintaan Persetujuan → Panggilan alat bernama
"request_approval"dengan detail persetujuan - Respons Persetujuan → Hasil Alat yang memuat keputusan pengguna
- Middleware → Menerjemahkan antara jenis Microsoft.Extensions.AI dan protokol AG-UI
Hal ini memungkinkan pola standar ApprovalRequiredAIFunction untuk bekerja di seluruh batas HTTP+SSE sambil mempertahankan konsistensi dengan model persetujuan kerangka kerja agen.
Pola Middleware Dua Arah
Middleware server dan klien mengikuti pola tiga langkah yang konsisten:
- Pesan Proses: Mengubah pesan masuk (respons persetujuan → FunctionApprovalResponseContent atau hasil alat)
- Memanggil Agen Dalam: Memanggil agen dalam dengan pesan yang diproses
- Pembaruan Proses: Mengubah pembaruan keluar (FunctionApprovalRequestContent → panggilan alat atau sebaliknya)
Pelacakan Status dengan AdditionalProperties
Alih-alih menggunakan kamus AdditionalProperties eksternal, implementasi menggunakan AIContent objek untuk melacak metadata.
-
Klien: Menyimpan
request_approval_call_iddiFunctionApprovalRequestContent.AdditionalProperties -
Preservasi Respons: Menyalin
AdditionalPropertiesdari permintaan ke respons untuk mempertahankan korelasi -
Konversi: Menggunakan CallId tersimpan untuk membuat hubungan yang berkorelasi dengan benar
FunctionResultContent
Ini menyimpan semua data korelasi dalam objek konten itu sendiri, menghindari kebutuhan akan manajemen status eksternal.
Pembersihan Pesan Server-Side
Middleware server harus menghapus pesan protokol persetujuan setelah memproses:
- Problem: Azure OpenAI mengharuskan semua pemanggilan alat memiliki hasil alat yang sesuai
- Solusi: Setelah mengonversi respons persetujuan, hapus baik panggilan alat maupun pesan hasilnya.
- Alasan: Mencegah kesalahan "tool_calls harus diikuti oleh pesan alat"
Langkah berikutnya
Tutorial ini menunjukkan kepada Anda cara menerapkan alur kerja human-in-the-loop dengan AG-UI, di mana pengguna harus menyetujui eksekusi alat sebelum dilakukan. Ini penting untuk operasi sensitif seperti transaksi keuangan, modifikasi data, atau tindakan yang memiliki konsekuensi signifikan.
Prasyarat
Sebelum memulai, pastikan Anda telah menyelesaikan tutorial Backend Tool Rendering dan memahami:
- Cara membuat alat fungsional
- Bagaimana AG-UI mengalirkan peristiwa alat
- Penyiapan server dan klien dasar
Apa itu Human-in-the-Loop?
Human-in-the-Loop (HITL) adalah pola di mana agen meminta persetujuan pengguna sebelum menjalankan operasi tertentu. Dengan AG-UI:
- Agen membuat panggilan alat seperti biasa
- Alih-alih segera dieksekusi, server mengirimkan permintaan persetujuan ke klien
- Aplikasi klien menampilkan permintaan dan meminta tanggapan dari pengguna
- Pengguna menyetujui atau menolak tindakan
- Server menerima respons dan melanjutkan sesuai prosedur
Keuntungan
- Keselamatan: Mencegah tindakan yang tidak diinginkan dijalankan
- Transparansi: Pengguna melihat dengan tepat apa yang ingin dilakukan agen
- Kontrol: Pengguna memiliki keputusan akhir atas operasi yang sensitif
- Kepatuhan: Memenuhi persyaratan peraturan untuk pengawasan manusia
Alat Penandaan untuk Persetujuan
Untuk memerlukan persetujuan untuk alat, gunakan approval_mode parameter di @tool dekorator:
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"
Mode Persetujuan
-
always_require: Selalu minta persetujuan sebelum eksekusi -
never_require: Jangan pernah meminta persetujuan (perilaku default) -
conditional: Meminta persetujuan berdasarkan kondisi tertentu (logika kustom)
Membuat Server dengan Human-in-the-Loop
Berikut adalah implementasi server lengkap dengan alat-alat yang memerlukan persetujuan:
"""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)
Konsep utama
-
AgentFrameworkAgentwrapper: Memungkinkan fitur protokol AG-UI seperti human-in-the-loop -
require_confirmation=True: Mengaktifkan alur kerja persetujuan untuk alat yang ditandai -
Kontrol tingkat alat: Hanya alat yang ditandai dengan
approval_mode="always_require"yang akan meminta persetujuan
Memahami Peristiwa Persetujuan
Saat alat memerlukan persetujuan, klien menerima peristiwa ini:
Peristiwa Permintaan Persetujuan
{
"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 Respons Persetujuan
Klien harus mengirim respons persetujuan:
# Approve
{
"type": "APPROVAL_RESPONSE",
"approvalId": "approval_abc123",
"approved": True
}
# Reject
{
"type": "APPROVAL_RESPONSE",
"approvalId": "approval_abc123",
"approved": False
}
Pelanggan dengan Fasilitas Persetujuan
Berikut adalah klien yang menggunakan AGUIChatClient yang menangani permintaan persetujuan:
"""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())
Contoh Interaksi
Dengan server dan klien yang berjalan:
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]
Jika pengguna menolak:
Approve this action? (yes/no): no
[Sending approval response: False]
I understand. The transfer has been cancelled and no money was moved.
[Run Finished]
Pesan Konfirmasi Kustom
Anda dapat menyesuaikan pesan persetujuan dengan memberikan strategi konfirmasi kustom:
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(),
)
Praktik Terbaik
Deskripsi Alat Yang Jelas
Berikan deskripsi terperinci sehingga pengguna memahami apa yang mereka setujui:
@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
Persetujuan Terperinci
Minta persetujuan untuk tindakan sensitif individu daripada pengelompokan.
# 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
Argumen Informatif
Gunakan nama parameter deskriptif dan berikan konteks:
@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
Penanganan Batas Waktu
Atur batas waktu yang sesuai untuk permintaan persetujuan:
# Client side
async with httpx.AsyncClient(timeout=120.0) as client: # 2 minutes for user to respond
# Handle approval
pass
Persetujuan Selektif
Anda dapat mencampur alat yang memerlukan persetujuan dengan alat yang tidak:
# 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