Human-in-the-Loop med AG-UI

MAF-verktygsgodkännande ansvarar fortfarande för att avgöra om ett verktyg kräver godkännande. AG-UI skickar godkännandebegäran till klienten och klientens beslut tillbaka till servern.

För godkännandeprinciper, villkorsregler och allmän säkerhetsvägledning, se Använda funktionsverktyg med godkännanden från människor i loopen.

Kräv godkännande

Omslut MAF-funktionen med ApprovalRequiredAIFunction och exponera agenten normalt:

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

AIFunction deleteFile = AIFunctionFactory.Create(
    (string path) => $"Deleted {path}",
    name: "delete_file",
    description: "Delete a file.");

AITool approvalRequiredTool = new ApprovalRequiredAIFunction(deleteFile);
AIAgent agent = chatClient.AsAIAgent(tools: [approvalRequiredTool]);

app.MapAGUIServer("/", agent);

När modellen anropar verktyget slutför AG-UI-adaptern körningen med ett avbrott i verktygsanropet i stället för att köra funktionen.

Lösa avbrott från en .NET-klient

AGUIChatClient visar avbrottet som ToolApprovalRequestContent. Skapa och skicka ett svar med de vanliga MAF-godkännandetyperna:

ToolApprovalRequestContent? request = null;

await foreach (AgentResponseUpdate update in
    remoteAgent.RunStreamingAsync(messages, session))
{
    request ??= update.Contents
        .OfType<ToolApprovalRequestContent>()
        .FirstOrDefault();
}

if (request is not null)
{
    ToolApprovalResponseContent response = request.CreateResponse(approved: true);
    ChatMessage resume = new(ChatRole.User, [response]);

    await foreach (AgentResponseUpdate update in
        remoteAgent.RunStreamingAsync([resume], session))
    {
        // Process the resumed response.
    }
}

Återanvänd samma AgentSession när du skickar svaret så att klienten kan fortsätta den avbrutna körningen. Använd approved: false för att avvisa anropet. Adaptern konverterar MAF-svaret till den kanoniska AG-UI-återupptagningsdatan.

Nästa steg

Den här handledningen visar hur du implementerar arbetsflöden för människa i processen med AG-UI, där användarna måste godkänna verktygsexekveringar innan de utförs. Detta är viktigt för känsliga åtgärder som finansiella transaktioner, dataändringar eller åtgärder som har betydande konsekvenser.

Förutsättningar

Innan du börjar bör du se till att du har slutfört handledningen för Backend Tool Rendering och förstått:

  • Så här skapar du funktionsverktyg
  • Så här AG-UI strömmar verktygshändelser
  • Grundläggande server- och klientkonfiguration

Vad är Human-in-the-Loop?

Human-in-the-Loop (HITL) är ett mönster där agenten begär användargodkännande innan vissa åtgärder körs. Med AG-UI:

  • Agenten genererar verktygsanrop som vanligt
  • I stället för att köra omedelbart skickar servern begäranden om godkännande till klienten
  • Klienten visar begäran och uppmanar användaren
  • Användaren godkänner eller avvisar åtgärden
  • Servern tar emot svaret och fortsätter därefter

Benefits

  • Säkerhet: Förhindra att oavsiktliga åtgärder utförs
  • Transparens: Användarna ser exakt vad agenten vill göra
  • Kontroll: Användarna har sista ord om känsliga åtgärder
  • Efterlevnad: Uppfylla regulatoriska krav för mänsklig tillsyn

Märkningsverktyg för godkännande

Om du vill kräva godkännande för ett verktyg använder du parametern approval_mode i dekoratören @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"

Godkännandelägen

  • always_require: Begär alltid godkännande före körning
  • never_require: Begär aldrig godkännande (standardbeteende)
  • conditional: Begär godkännande baserat på vissa villkor (anpassad logik)

Skapa en server med Human-in-the-Loop

Här är en fullständig serverimplementering med verktyg som krävs för godkännande:

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

Viktiga begrepp

  • AgentFrameworkAgent wrapper: Aktiverar AG-UI protokollfunktioner som human-in-the-loop
  • require_confirmation=True: Aktiverar arbetsflöde för godkännande för markerade verktyg
  • Kontroll på verktygsnivå: Endast verktyg som markerats med approval_mode="always_require" begär godkännande

Förstå avbrott i godkännanden

När ett verktyg kräver godkännande, avslutas körningen med ett standardiserat AG-UI-avbrott.

Avbrott i godkännande

{
  "type": "RUN_FINISHED",
  "threadId": "thread-1",
  "runId": "run-1",
  "outcome": {
    "type": "interrupt",
    "interrupts": [
      {
        "id": "approval-1",
        "reason": "tool_call",
        "message": "Approve tool call transfer_money?",
        "toolCallId": "call-1",
        "responseSchema": {
          "type": "object",
          "properties": {
            "accepted": { "type": "boolean" },
            "arguments": { "type": "object" }
          },
          "required": ["accepted"]
        },
        "metadata": {
          "agent_framework": {
            "type": "function_approval_request",
            "function_call": {
              "call_id": "call-1",
              "name": "transfer_money",
              "arguments": {
                "from_account": "1234567890",
                "to_account": "0987654321",
                "amount": 500.00,
                "currency": "USD"
              }
            }
          }
        }
      }
    ]
  }
}

Verktygsgodkännande avbryter användningen av reason: "tool_call" och inkluderar en toolCallId. Den slutliga ChatResponseUpdate från AGUIChatClient bevarar värdena för outcome och interrupts i additional_properties. Interrupt och ResumeEntry är protokolltyper från ag_ui.core, inte Agent Framework-specifika modeller.

CV-format

Återuppta samma tråd med en kanonisk resume matris. Använd accepted: false för att avvisa åtgärden samtidigt som agenten kan fortsätta. Använd status: "cancelled" utan nyttolast för att avbryta den avbrutna körningen.

{
  "threadId": "thread-1",
  "messages": [],
  "resume": [
    {
      "interruptId": "approval-1",
      "status": "resolved",
      "payload": {
        "accepted": true
      }
    }
  ]
}

Klient med godkännande stöd

Här är en klient som använder AGUIChatClient som hanterar begäranden om godkännande:

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

import asyncio
import os

from agent_framework import Agent
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 content.type == "function_approval_request":
            function_call = content.function_call
            print(f"\nAction {i}:")
            print(f"  Tool: \033[95m{function_call.name}\033[0m")
            print(f"  Arguments:")
            for key, value in (function_call.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(endpoint=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_interrupts = []

            async for update in agent.run(message, session=thread, stream=True):
                # Check if this update carries an approval request.
                if any(content.type == "function_approval_request" for content in update.contents):
                    display_approval_request(update)

                if update.text:
                    print(f"\033[96m{update.text}\033[0m", end="", flush=True)

                properties = update.additional_properties or {}
                outcome = properties.get("outcome")
                if isinstance(outcome, dict) and outcome.get("type") == "interrupt":
                    pending_interrupts = outcome.get("interrupts", [])

            if pending_interrupts:
                resume_entries = []
                for interrupt in pending_interrupts:
                    prompt = interrupt.get("message", "Approve this action?")
                    user_choice = input(f"\n{prompt} (yes/no): ").strip().lower()
                    resume_entries.append({
                        "interruptId": interrupt["id"],
                        "status": "resolved",
                        "payload": {"accepted": user_choice in ("yes", "y")},
                    })

                print("\nAssistant: ", end="", flush=True)
                async for update in agent.run(
                    [],
                    session=thread,
                    stream=True,
                    options={
                        "available_interrupts": pending_interrupts,
                        "resume": resume_entries,
                    },
                ):
                    if update.text:
                        print(f"\033[96m{update.text}\033[0m", end="", flush=True)

            print()

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


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

Exempelinteraktion

När servern och klienten körs:

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]

Om användaren avvisar:

Approve this action? (yes/no): no

[Sending approval response: False]

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

Anpassade bekräftelsemeddelanden

Anpassa godkännande- och bekräftelsemeddelanden i ditt AG-UI-klientgränssnitt vid rendering av godkännandeavbrott från servern. Python AgentFrameworkAgent exponerar begäranden om godkännande och avbryter metadata. Det krävs inte ett bekräftelsestrategiobjekt på serversidan.

Metodtips

Rensa verktygsbeskrivningar

Ange detaljerade beskrivningar så att användarna förstår vad de godkänner:

@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

Finkornigt godkännande

Begär godkännande för enskilda känsliga åtgärder i stället för batchbearbetning:

# 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

Informativa argumenten

Använd beskrivande parameternamn och ange 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

Timeouthantering

Ange lämpliga tidsgränser för godkännandebegäranden:

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

Selektivt godkännande

Du kan blanda verktyg som kräver godkännande med de som inte gör det:

# 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

Gruppvisa godkännanden och avbrytande

Ett modellsvar kan innehålla både verktyg som krävs för godkännande och verktyg som inte kräver godkännande. Om du löser det synliga avbrottet slutförs även de andra verktygsanropen från den batchen enligt deras godkännandebeslut. Till exempel körs en never_require parallell gren och dess TOOL_CALL_RESULT strömmas vidare i den återupptagna körningen även när den parallella gren som kräver godkännande avvisas.

Att avbryta med status: "cancelled" avbryter återupptagningen av godkännandet och rensar det köade godkännandetillståndet för tråden. Senare begäranden kan inte dyka upp igen eller köra inaktuella verktygsanrop från den avbrutna batchen.

Nästa steg

Ytterligare resurser

Go stöder AG-UI-arbetsflöden med mänsklig medverkan och verktyg som kräver godkännande. Omslut ett funktionsverktyg med tool.ApprovalRequiredFuncoch var sedan värd för agenten via aguiprovider.

approveExpense := functool.MustNew(functool.Config{
    Name:        "approve_expense_report",
    Description: "Approve the expense report.",
}, func(ctx context.Context, expenseReportID string) (string, error) {
    return fmt.Sprintf("Expense report %s approved", expenseReportID), nil
})

a := foundryprovider.NewAgent(endpoint, token, foundryprovider.ModelDeployment(model), foundryprovider.AgentConfig{
    Config: agent.Config{
        Tools: []tool.Tool{tool.ApprovalRequiredFunc(approveExpense)},
    },
})

Tip

Se AG-UI human-in-the-loop-exemplet för ett fullständigt körbart exempel.