Udostępnij przez


Renderowanie narzędzi frontonu za pomocą AG-UI

W tym samouczku pokazano, jak dodać narzędzia funkcji frontonu do klientów AG-UI. Narzędzia frontonu to funkcje wykonywane po stronie klienta, które umożliwiają agentowi sztucznej inteligencji interakcję ze środowiskiem lokalnym użytkownika, uzyskiwanie dostępu do danych specyficznych dla klienta lub wykonywanie operacji interfejsu użytkownika. Serwer organizuje, kiedy wywołać te narzędzia, ale wykonywanie odbywa się całkowicie na kliencie.

Wymagania wstępne

Przed rozpoczęciem upewnij się, że ukończyłeś samouczek Wprowadzenie i masz następujące elementy:

  • .NET 8.0 lub nowszy
  • Microsoft.Agents.AI.AGUI pakiet zainstalowany
  • Microsoft.Agents.AI pakiet zainstalowany
  • Podstawowa wiedza na temat konfiguracji klienta AG-UI

Co to są narzędzia frontonu?

Narzędzia frontonu to narzędzia funkcji, które:

  • Są definiowane i rejestrowane na kliencie
  • Wykonywanie w środowisku klienta (nie na serwerze)
  • Zezwalanie agentowi sztucznej inteligencji na interakcję z zasobami specyficznymi dla klienta
  • Podaj wyniki z powrotem do serwera agenta, aby uwzględnić je w odpowiedziach
  • Włączanie spersonalizowanych, świadomych kontekstu doświadczeń

Typowe przypadki użycia:

  • Odczytywanie danych z czujników lokalnych (GPS, temperatura itp.)
  • Uzyskiwanie dostępu do magazynu lub preferencji po stronie klienta
  • Wykonywanie operacji interfejsu użytkownika (zmienianie motywów, wyświetlanie powiadomień)
  • Interakcja z funkcjami specyficznymi dla urządzenia (aparat, mikrofon)

Rejestrowanie narzędzi frontonu na kliencie

Kluczową różnicą w samouczku 'Wprowadzenie' jest rejestrowanie narzędzi przy użyciu agenta klienta. Oto co się zmienia:

// 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);

Pozostała część kodu klienta pozostaje taka sama, jak pokazano w samouczku Wprowadzenie.

Jak narzędzia są wysyłane do serwera

Podczas rejestrowania narzędzi w systemie za pomocą AsAIAgent(), AGUIChatClient automatycznie:

  1. Przechwytuje definicje narzędzi (nazwy, opisy, schematy parametrów)
  2. Wysyła narzędzia z każdym żądaniem do agenta serwera, który mapuje je na ChatAgentRunOptions.ChatOptions.Tools

Serwer odbiera deklaracje narzędzi klienta, a model sztucznej inteligencji może zdecydować, kiedy je wywołać.

Inspekcja i modyfikowanie narzędzi za pomocą oprogramowania pośredniczącego

Można użyć oprogramowania pośredniczącego dla agenta, aby sprawdzić lub zmodyfikować jego działanie, w tym uzyskać dostęp do narzędzi:

// 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;
    }
}

Ten wzorzec oprogramowania pośredniczącego umożliwia:

  • Weryfikowanie definicji narzędzi przed wykonaniem

Kluczowe pojęcia

Poniżej przedstawiono nowe pojęcia dotyczące narzędzi frontonu:

  • Rejestracja po stronie klienta: narzędzia są rejestrowane na kliencie przy użyciu AIFunctionFactory.Create() i przekazywane do AsAIAgent()
  • Automatyczne przechwytywanie: narzędzia są automatycznie przechwytywane i wysyłane za pośrednictwem ChatAgentRunOptions.ChatOptions.Tools

Jak działają narzędzia frontonu

przepływ po stronie serwera

Serwer nie zna szczegółów implementacji narzędzi frontonu. Wie tylko:

  1. Nazwy i opisy narzędzi (z rejestracji klienta)
  2. Schematy parametrów
  3. Kiedy zażądać wykonania narzędzia

Gdy agent sztucznej inteligencji zdecyduje się wywołać narzędzie frontendowe:

  1. Serwer wysyła żądanie wywołania narzędzia do klienta za pośrednictwem protokołu SSE
  2. Serwer czeka na wykonanie narzędzia przez klienta i zwrócenie wyników
  3. Serwer włącza wyniki do kontekstu agenta
  4. Agent kontynuuje przetwarzanie za pomocą wyników narzędzia

Przepływ po stronie klienta

Klient obsługuje działanie narzędzi frontendowych.

  1. Odbiera FunctionCallContent z serwera wskazujące żądanie wywołania narzędzia
  2. Dopasuje nazwę narzędzia do lokalnie zarejestrowanej funkcji
  3. Deserializuje parametry z żądania
  4. Wykonuje funkcję lokalnie
  5. Serializuje wynik
  6. Wysyła FunctionResultContent z powrotem do serwera
  7. Kontynuuje odbieranie odpowiedzi agenta

Oczekiwane dane wyjściowe z narzędziami frontendu

Gdy agent wywołuje narzędzia frontendowe, zobaczysz wywołanie narzędzia i wynik w strumieniowym wyjściu.

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.

Konfiguracja serwera dla narzędzi frontendowych

Serwer nie potrzebuje specjalnej konfiguracji do obsługi narzędzi frontonu. Użyj standardowego serwera AG-UI z samouczka 'Pierwsze kroki', który automatycznie:

  • Odbiera deklaracje narzędzi frontonu podczas połączenia klienta
  • Żąda wykonania narzędzia, gdy agent sztucznej inteligencji tego potrzebuje
  • Czeka na wyniki od klienta
  • Uwzględnia wyniki w podejmowaniu decyzji agenta

Dalsze kroki

Teraz, gdy znasz narzędzia frontonu, możesz:

Dodatkowe zasoby

W tym samouczku pokazano, jak dodać narzędzia funkcji frontonu do klientów AG-UI. Narzędzia frontonu to funkcje wykonywane po stronie klienta, które umożliwiają agentowi sztucznej inteligencji interakcję ze środowiskiem lokalnym użytkownika, uzyskiwanie dostępu do danych specyficznych dla klienta lub wykonywanie operacji interfejsu użytkownika.

Wymagania wstępne

Przed rozpoczęciem upewnij się, że ukończyłeś samouczek Wprowadzenie i masz następujące elementy:

  • Środowisko Python w wersji 3.10 lub nowszej
  • httpx zainstalowane na potrzeby funkcji klienta HTTP
  • Podstawowa wiedza na temat konfiguracji klienta AG-UI
  • Skonfigurowano usługę Azure OpenAI

Co to są narzędzia frontonu?

Narzędzia frontonu to narzędzia funkcji, które:

  • Są definiowane i rejestrowane na kliencie
  • Wykonywanie w środowisku klienta (nie na serwerze)
  • Zezwalanie agentowi sztucznej inteligencji na interakcję z zasobami specyficznymi dla klienta
  • Podaj wyniki z powrotem do serwera agenta, aby uwzględnić je w odpowiedziach

Typowe przypadki użycia:

  • Odczytywanie danych z czujników lokalnych
  • Uzyskiwanie dostępu do magazynu lub preferencji po stronie klienta
  • Wykonywanie operacji interfejsu użytkownika
  • Interakcja z funkcjami specyficznymi dla urządzenia

Tworzenie narzędzi frontonu

Narzędzia frontonu w języku Python są definiowane podobnie do narzędzi zaplecza, ale są zarejestrowane w kliencie:

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

Tworzenie klienta AG-UI za pomocą narzędzi frontonu

Oto kompletna implementacja klienta z narzędziami frontonu:

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

Jak działają narzędzia frontonu

Przepływ protokołu

  1. Rejestracja klienta: klient wysyła deklaracje narzędzi (nazwy, opisy, parametry) do serwera
  2. Orkiestracja serwera: agent sztucznej inteligencji decyduje, kiedy wywołać narzędzia frontonu na podstawie żądania użytkownika
  3. Żądanie wywołania narzędzia: serwer wysyła TOOL_CALL_REQUEST zdarzenie do klienta za pośrednictwem usługi SSE
  4. Wykonywanie klienta: klient wykonuje narzędzie lokalnie
  5. Przesyłanie wyników: klient wysyła wynik z powrotem do serwera za pośrednictwem żądania POST
  6. Przetwarzanie agenta: serwer integruje wynik i kontynuuje przetwarzanie odpowiedzi

Kluczowe zdarzenia

  • TOOL_CALL_REQUEST: Serwer żąda wykonania narzędzia frontendowego
  • TOOL_CALL_RESULT: Klient przesyła wynik wykonania (za pośrednictwem protokołu HTTP POST)

Oczekiwane dane wyjściowe

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]

Konfiguracja serwera

Standardowy serwer AG-UI z samouczka 'Pierwsze Kroki' automatycznie obsługuje narzędzia frontendowe. Po stronie serwera nie są wymagane żadne zmiany — automatycznie obsługuje orkiestrację narzędzi.

Najlepsze praktyki

Zabezpieczenia

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"

Obsługa błędów

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

Operacje asynchroniczne

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

Rozwiązywanie problemów

Narzędzia, które nie są wywoływane

  1. Upewnij się, że deklaracje narzędzi są wysyłane do serwera
  2. Sprawdź, czy opisy narzędzi wyraźnie wskazują cel
  3. Sprawdzanie dzienników serwera pod kątem rejestracji narzędzi

Błędy wykonawcze

  1. Dodawanie kompleksowej obsługi błędów
  2. Weryfikowanie parametrów przed przetworzeniem
  3. Zwracanie przyjaznych dla użytkownika komunikatów o błędach
  4. Rejestrowanie błędów na potrzeby debugowania

Problemy typowe

  1. Używanie modeli Pydantic dla typów złożonych
  2. Konwertowanie modeli na dykty przed serializacji
  3. Jawnie przeprowadzaj konwersje typów

Dalsze kroki

Dodatkowe zasoby