Dela via


Klientdelsverktygsrendering med AG-UI

Den här handledningen visar hur du lägger till frontend-funktionsverktyg för dina AG-UI-klienter. Klientdelsverktyg är funktioner som körs på klientsidan, vilket gör att AI-agenten kan interagera med användarens lokala miljö, komma åt klientspecifika data eller utföra användargränssnittsåtgärder. Servern orkestrerar när dessa verktyg ska anropas, men körningen sker helt på klienten.

Förutsättningar

Innan du börjar, se till att du har slutfört självstudien "Komma igång" och:

  • .NET 8.0 eller senare
  • Microsoft.Agents.AI.AGUI paketet är installerat
  • Microsoft.Agents.AI paketet är installerat
  • Grundläggande förståelse för AG-UI klientkonfiguration

Vad är Frontend-verktyg?

Frontend-verktyg är funktionella verktyg som

  • Definieras och registreras på klienten
  • Kör i klientens miljö (inte på servern)
  • Tillåt ATT AI-agenten interagerar med klientspecifika resurser
  • Ange resultat tillbaka till servern så att agenten kan införliva i svar
  • Aktivera anpassade, kontextmedvetna upplevelser

Vanliga användningsfall:

  • Läsa lokala sensordata (GPS, temperatur osv.)
  • Åtkomst till lagring på klientsidan eller inställningar
  • Utföra användargränssnittsåtgärder (ändra teman, visa meddelanden)
  • Interagera med enhetsspecifika funktioner (kamera, mikrofon)

Registrera frontend-verktyg på klienten

Den viktigaste skillnaden från introduktionsguiden är att registrera verktygen med klientagenten. Här är vad som ändras:

// Define a frontend function tool
[Description("Get the user's current location from GPS.")]
static string GetUserLocation()
{
    // Access client-side GPS
    return "Amsterdam, Netherlands (52.37°N, 4.90°E)";
}

// Create frontend tools
AITool[] frontendTools = [AIFunctionFactory.Create(GetUserLocation)];

// Pass tools when creating the agent
AIAgent agent = chatClient.AsAIAgent(
    name: "agui-client",
    description: "AG-UI Client Agent",
    tools: frontendTools);

Resten av klientkoden förblir densamma som i självstudien "Komma igång".

Så här skickas verktyg till servern

När du registrerar verktyg med AsAIAgent(), utför AGUIChatClient automatiskt:

  1. Samlar in verktygsdefinitionerna (namn, beskrivningar, parameterscheman)
  2. Skickar verktygen med varje begäran till serveragenten som mappar dem till ChatAgentRunOptions.ChatOptions.Tools

Servern tar emot klientverktygsdeklarationerna och AI-modellen kan bestämma när de ska anropas.

Inspektera och ändra verktyg med mellanprogram

Du kan använda agentmellanprogram för att inspektera eller ändra agentkörningen, inklusive åtkomst till verktygen:

// Create agent with middleware that inspects tools
AIAgent inspectableAgent = baseAgent
    .AsBuilder()
    .Use(runFunc: null, runStreamingFunc: InspectToolsMiddleware)
    .Build();

static async IAsyncEnumerable<AgentResponseUpdate> InspectToolsMiddleware(
    IEnumerable<ChatMessage> messages,
    AgentSession? session,
    AgentRunOptions? options,
    AIAgent innerAgent,
    CancellationToken cancellationToken)
{
    // Access the tools from ChatClientAgentRunOptions
    if (options is ChatClientAgentRunOptions chatOptions)
    {
        IList<AITool>? tools = chatOptions.ChatOptions?.Tools;
        if (tools != null)
        {
            Console.WriteLine($"Tools available for this run: {tools.Count}");
            foreach (AITool tool in tools)
            {
                if (tool is AIFunction function)
                {
                    Console.WriteLine($"  - {function.Metadata.Name}: {function.Metadata.Description}");
                }
            }
        }
    }

    await foreach (AgentResponseUpdate update in innerAgent.RunStreamingAsync(messages, session, options, cancellationToken))
    {
        yield return update;
    }
}

Med det här mellanprogramsmönstret kan du:

  • Verifiera verktygsdefinitioner före implementering

Viktiga begrepp

Följande är nya begrepp för klientdelsverktyg:

  • Registrering på klientsidan: Verktyg registreras på klienten med hjälp av AIFunctionFactory.Create() och skickas till AsAIAgent()
  • Automatisk upptagning: Verktyg samlas in automatiskt och skickas via ChatAgentRunOptions.ChatOptions.Tools

Så här fungerar klientdelsverktyg

Flöde på Serversidan

Servern känner inte till implementeringsinformationen för klientdelsverktyg. Den vet bara:

  1. Verktygsnamn och beskrivningar (från klientregistrering)
  2. Parameterscheman
  3. När man ska begära verktygsutförande

När AI-agenten bestämmer sig för att anropa ett klientdelsverktyg:

  1. Servern skickar en begäran om verktygsanrop till klienten via SSE
  2. Servern väntar på att klienten ska köra verktyget och returnera resultat
  3. Servern införlivar resultaten i agentens kontext
  4. Agenten fortsätter bearbetningen med verktygsresultatet

Klientsideflöde

Klienten hanterar körning av gränssnittsverktyg.

  1. FunctionCallContent Tar emot från servern som anger en begäran om verktygsanrop
  2. Matchar verktygsnamnet med en lokalt registrerad funktion
  3. Deserialiserar parametrar från begäran
  4. Kör funktionen lokalt
  5. Serialiserar resultatet
  6. Skickar FunctionResultContent tillbaka till servern
  7. Fortsätter att ta emot svar från agenter

Förväntade utdata med klientdelsverktyg

När agenten anropar klientdelsverktygen ser du verktygets anrop och resulterar i strömmande utdata:

User (:q or quit to exit): Where am I located?

[Client Tool Call - Name: GetUserLocation]
[Client Tool Result: Amsterdam, Netherlands (52.37°N, 4.90°E)]

You are currently in Amsterdam, Netherlands, at coordinates 52.37°N, 4.90°E.

Serverinställning för frontendverktyg

Servern behöver inte någon särskild konfiguration för att stödja klientdelsverktyg. Använd standardservern AG-UI från Komma igång med självstudiekursen – den fungerar automatiskt:

  • Tar emot frontend-verktygsdeklarationer vid klientanslutningen
  • Begär körning av verktyg när AI-agenten behöver dem
  • Väntar på resultat från klienten
  • Innehåller resultat i agentens beslutsfattande

Nästa steg

Nu när du förstår klientdelsverktygen kan du:

Ytterligare resurser

Den här handledningen visar hur du lägger till frontend-funktionsverktyg för dina AG-UI-klienter. Klientdelsverktyg är funktioner som körs på klientsidan, vilket gör att AI-agenten kan interagera med användarens lokala miljö, komma åt klientspecifika data eller utföra användargränssnittsåtgärder.

Förutsättningar

Innan du börjar, se till att du har slutfört självstudien "Komma igång" och:

  • Python 3.10 eller senare
  • httpx installerat för HTTP-klientfunktioner
  • Grundläggande förståelse för AG-UI klientkonfiguration
  • Azure OpenAI-tjänsten har konfigurerats

Vad är Frontend-verktyg?

Frontend-verktyg är funktionella verktyg som

  • Definieras och registreras på klienten
  • Kör i klientens miljö (inte på servern)
  • Tillåt ATT AI-agenten interagerar med klientspecifika resurser
  • Ange resultat tillbaka till servern så att agenten kan införliva i svar

Vanliga användningsfall:

  • Läsa lokala sensordata
  • Åtkomst till lagring på klientsidan eller inställningar
  • Utföra användargränssnittsåtgärder
  • Interagera med enhetsspecifika funktioner

Skapa frontendverktyg

Klientdelsverktyg i Python definieras på samma sätt som serverdelsverktyg men registreras med klienten:

from typing import Annotated
from pydantic import BaseModel, Field


class SensorReading(BaseModel):
    """Sensor reading from client device."""
    temperature: float
    humidity: float
    air_quality_index: int


def read_climate_sensors(
    include_temperature: Annotated[bool, Field(description="Include temperature reading")] = True,
    include_humidity: Annotated[bool, Field(description="Include humidity reading")] = True,
) -> SensorReading:
    """Read climate sensor data from the client device."""
    # Simulate reading from local sensors
    return SensorReading(
        temperature=22.5 if include_temperature else 0.0,
        humidity=45.0 if include_humidity else 0.0,
        air_quality_index=75,
    )


def change_background_color(color: Annotated[str, Field(description="Color name")] = "blue") -> str:
    """Change the console background color."""
    # Simulate UI change
    print(f"\n🎨 Background color changed to {color}")
    return f"Background changed to {color}"

Skapa en AG-UI-klient med frontend-verktyg

Här är en komplett klientimplementering med frontend-verktyg:

"""AG-UI client with frontend tools."""

import asyncio
import json
import os
from typing import Annotated, AsyncIterator

import httpx
from pydantic import BaseModel, Field


class SensorReading(BaseModel):
    """Sensor reading from client device."""
    temperature: float
    humidity: float
    air_quality_index: int


# Define frontend tools
def read_climate_sensors(
    include_temperature: Annotated[bool, Field(description="Include temperature")] = True,
    include_humidity: Annotated[bool, Field(description="Include humidity")] = True,
) -> SensorReading:
    """Read climate sensor data from the client device."""
    return SensorReading(
        temperature=22.5 if include_temperature else 0.0,
        humidity=45.0 if include_humidity else 0.0,
        air_quality_index=75,
    )


def get_user_location() -> dict:
    """Get the user's current GPS location."""
    # Simulate GPS reading
    return {
        "latitude": 52.3676,
        "longitude": 4.9041,
        "accuracy": 10.0,
        "city": "Amsterdam",
    }


# Tool registry maps tool names to functions
FRONTEND_TOOLS = {
    "read_climate_sensors": read_climate_sensors,
    "get_user_location": get_user_location,
}


class AGUIClientWithTools:
    """AG-UI client with frontend tool support."""

    def __init__(self, server_url: str, tools: dict):
        self.server_url = server_url
        self.tools = tools
        self.thread_id: str | None = None

    async def send_message(self, message: str) -> AsyncIterator[dict]:
        """Send a message and handle streaming response with tool execution."""
        # Prepare tool declarations for the server
        tool_declarations = []
        for name, func in self.tools.items():
            tool_declarations.append({
                "name": name,
                "description": func.__doc__ or "",
                # Add parameter schema from function signature
            })

        request_data = {
            "messages": [
                {"role": "system", "content": "You are a helpful assistant with access to client tools."},
                {"role": "user", "content": message},
            ],
            "tools": tool_declarations,  # Send tool declarations to server
        }

        if self.thread_id:
            request_data["thread_id"] = self.thread_id

        async with httpx.AsyncClient(timeout=60.0) as client:
            async with client.stream(
                "POST",
                self.server_url,
                json=request_data,
                headers={"Accept": "text/event-stream"},
            ) as response:
                response.raise_for_status()

                async for line in response.aiter_lines():
                    if line.startswith("data: "):
                        data = line[6:]
                        try:
                            event = json.loads(data)

                            # Handle tool call requests from server
                            if event.get("type") == "TOOL_CALL_REQUEST":
                                await self._handle_tool_call(event, client)
                            else:
                                yield event

                            # Capture thread_id
                            if event.get("type") == "RUN_STARTED" and not self.thread_id:
                                self.thread_id = event.get("threadId")

                        except json.JSONDecodeError:
                            continue

    async def _handle_tool_call(self, event: dict, client: httpx.AsyncClient):
        """Execute frontend tool and send result back to server."""
        tool_name = event.get("toolName")
        tool_call_id = event.get("toolCallId")
        arguments = event.get("arguments", {})

        print(f"\n\033[95m[Client Tool Call: {tool_name}]\033[0m")
        print(f"  Arguments: {arguments}")

        try:
            # Execute the tool
            tool_func = self.tools.get(tool_name)
            if not tool_func:
                raise ValueError(f"Unknown tool: {tool_name}")

            result = tool_func(**arguments)

            # Convert Pydantic models to dict
            if hasattr(result, "model_dump"):
                result = result.model_dump()

            print(f"\033[94m[Client Tool Result: {result}]\033[0m")

            # Send result back to server
            await client.post(
                f"{self.server_url}/tool_result",
                json={
                    "tool_call_id": tool_call_id,
                    "result": result,
                },
            )

        except Exception as e:
            print(f"\033[91m[Tool Error: {e}]\033[0m")
            # Send error back to server
            await client.post(
                f"{self.server_url}/tool_result",
                json={
                    "tool_call_id": tool_call_id,
                    "error": str(e),
                },
            )


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

    client = AGUIClientWithTools(server_url, FRONTEND_TOOLS)

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

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

            print()
            async for event in client.send_message(message):
                event_type = event.get("type", "")

                if event_type == "RUN_STARTED":
                    print(f"\033[93m[Run Started]\033[0m")

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

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

Så här fungerar klientdelsverktyg

Protokollflöde

  1. Klientregistrering: Klienten skickar verktygsdeklarationer (namn, beskrivningar, parametrar) till servern
  2. Serverorkestrering: AI-agenten bestämmer när klientdelsverktyg ska anropas baserat på användarbegäran
  3. Begäran om verktygsanrop: Servern skickar TOOL_CALL_REQUEST händelsen till klienten via SSE
  4. Klientkörning: Klienten kör verktyget lokalt
  5. Resultatöverföring: Klienten skickar tillbaka resultatet till servern via POST-begäran
  6. Agentbearbetning: Servern inkorporerar resultatet och fortsätter med att svara

Viktiga händelser

  • TOOL_CALL_REQUEST: Server begär körning av frontend-verktyget
  • TOOL_CALL_RESULT: Klienten skickar körningsresultat (via HTTP POST)

Förväntade utdata

User (:q or quit to exit): What's the temperature reading from my sensors?

[Run Started]

[Client Tool Call: read_climate_sensors]
  Arguments: {'include_temperature': True, 'include_humidity': True}
[Client Tool Result: {'temperature': 22.5, 'humidity': 45.0, 'air_quality_index': 75}]

Based on your sensor readings, the current temperature is 22.5°C and the 
humidity is at 45%. These are comfortable conditions!
[Run Finished]

Serverkonfiguration

Standardservern AG-UI från kom igång-självstudien stöder automatiskt gränssnittsverktyg. Inga ändringar behövs på serversidan – den hanterar verktygsorkestrering automatiskt.

Metodtips

Security

def access_sensitive_data() -> str:
    """Access user's sensitive data."""
    # Always check permissions first
    if not has_permission():
        return "Error: Permission denied"

    try:
        # Access data
        return "Data retrieved"
    except Exception as e:
        # Don't expose internal errors
        return "Unable to access data"

Felhantering

def read_file(path: str) -> str:
    """Read a local file."""
    try:
        with open(path, "r") as f:
            return f.read()
    except FileNotFoundError:
        return f"Error: File not found: {path}"
    except PermissionError:
        return f"Error: Permission denied: {path}"
    except Exception as e:
        return f"Error reading file: {str(e)}"

Asynkrona åtgärder

async def capture_photo() -> str:
    """Capture a photo from device camera."""
    # Simulate camera access
    await asyncio.sleep(1)
    return "photo_12345.jpg"

Felsökning

Verktyg som inte anropas

  1. Se till att verktygsdeklarationer skickas till servern
  2. Kontrollera verktygsbeskrivningar som tydligt anger syftet
  3. Kontrollera om det finns verktygsregistrering i serverloggarna

Körningsfel

  1. Lägga till omfattande felhantering
  2. Verifiera parametrar före bearbetning
  3. Returnera användarvänliga felmeddelanden
  4. Loggfel för felsökning

Typproblem

  1. Använda pydantiska modeller för komplexa typer
  2. Konvertera modeller till ordlistor före serialisering
  3. Hantera typkonverteringar explicit.

Nästa steg

Ytterligare resurser