Rendu de l’outil frontal avec AG-UI

Les outils frontaux sont déclarés et exécutés par le client AG-UI. Le serveur reçoit ses schémas afin que le modèle puisse les demander, mais il ne reçoit pas ses implémentations.

Enregistrer un outil front-end

Créez l’outil et passez-le à l’agent soutenu par AGUIChatClient :

using System.ComponentModel;
using AGUI.Client;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;

[Description("Get the user's current location from the client device.")]
static string GetUserLocation() => "Amsterdam, Netherlands";

AITool locationTool = AIFunctionFactory.Create(
    GetUserLocation,
    name: "get_user_location");

using HttpClient httpClient = new() { BaseAddress = new Uri("http://localhost:8888") };
AGUIChatClient chatClient = new(new AGUIChatClientOptions(httpClient, "/"));
AIAgent agent = chatClient.AsAIAgent(tools: [locationTool]);

AGUIChatClient gère le processus de continuation :

  1. Envoie la déclaration de l’outil d’interface utilisateur avec la requête d’exécution.
  2. Reçoit l’appel à l’outil du modèle depuis le serveur.
  3. Exécute la fonction correspondante localement.
  4. Renvoie le résultat au serveur.
  5. Poursuit l’exécution et transmet la réponse finale en continu.

Tip

Consultez l’exemple .NET d’outils frontend pour un client et un serveur complets.

Avertissement

Les déclarations d’outils et les résultats fournis par un client non approuvé sont des entrées non approuvées. Autorisez les outils clients qui peuvent influencer l’exécution de l’agent côté serveur et valider les résultats avant de les utiliser pour les opérations privilégiées.

Pour obtenir des conseils généraux sur la création d’outils, consultez Utiliser des outils de fonction avec un agent.

Étapes suivantes

Ce tutoriel vous montre comment ajouter des outils de fonction front-end à vos clients AG-UI. Les outils frontaux sont des fonctions qui s’exécutent côté client, ce qui permet à l’agent IA d’interagir avec l’environnement local de l’utilisateur, d’accéder aux données spécifiques au client ou d’effectuer des opérations d’interface utilisateur.

Prerequisites

Avant de commencer, vérifiez que vous avez terminé le didacticiel De prise en main et que vous disposez des points suivants :

  • Python 3.10 ou version ultérieure
  • httpx installé pour la fonctionnalité client HTTP
  • Compréhension de base de la configuration du client AG-UI
  • Le service OpenAI d'Azure est configuré

Qu’est-ce que les outils frontaux ?

Les outils frontaux sont des outils de fonction qui :

  • Sont définis et inscrits sur le client
  • Exécuter dans l’environnement du client (et non sur le serveur)
  • Autoriser l’agent IA à interagir avec des ressources spécifiques au client
  • Fournir des résultats au serveur afin que l'agent puisse les intégrer dans ses réponses.

Cas d’usage courants :

  • Lecture des données de capteur local
  • Accès au stockage ou aux préférences côté client
  • Exécution d’opérations d’interface utilisateur
  • Interaction avec des fonctionnalités spécifiques à l’appareil

Création d’outils frontaux

Les outils front-end dans Python sont définis de la même façon que les outils principaux, mais sont inscrits auprès du client :

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

Création d’un client AG-UI avec les outils front-end

Voici une implémentation complète du client avec les outils front-end :

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

                            # Tool calls arrive as TOOL_CALL_START/ARGS/END events
                            # and results are streamed back as TOOL_CALL_RESULT events.
                            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")

            # In current Python AG-UI, frontend tool declarations are sent with
            # the run request. Tool-call lifecycle events are streamed back over SSE.
            print(f"Tool result for {tool_call_id}: {result}")

        except Exception as e:
            print(f"\033[91m[Tool Error: {e}]\033[0m")
            print(f"Tool error for {tool_call_id}: {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())

Fonctionnement des outils frontaux

Flux de protocole

  1. Inscription du client : le client envoie des déclarations d’outils (noms, descriptions, paramètres) au serveur
  2. Orchestration de serveur : l’agent IA décide quand appeler des outils front-end en fonction de la demande de l’utilisateur
  3. Événements d’appel d’outil : le serveur transmet en flux continu les événements TOOL_CALL_START, TOOL_CALL_ARGS et TOOL_CALL_END au client
  4. Exécution du client : le client exécute l’outil localement
  5. Événements de résultat : les résultats de l’outil sont représentés en tant qu’événements TOOL_CALL_RESULT dans le flux
  6. Traitement de l’agent : le serveur intègre les résultats et poursuit la réponse

Événements clés

  • TOOL_CALL_START / TOOL_CALL_ARGS / TOOL_CALL_END: Le serveur demande et diffuse en continu les détails des appels d’outils
  • TOOL_CALL_RESULT: événement de résultat de l’exécution de l’outil

Sortie attendue

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]

Configuration du serveur

Le serveur AG-UI standard à partir du didacticiel de prise en main prend automatiquement en charge les outils frontend. Aucune modification n’est nécessaire côté serveur : elle gère automatiquement l’orchestration des outils.

Meilleures pratiques

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"

Gestion des erreurs

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

Opérations asynchrones

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

Troubleshooting

Outils non invoqués

  1. Vérifier que les déclarations d’outil sont envoyées au serveur
  2. Vérifiez que les descriptions des outils indiquent clairement leur objectif.
  3. Vérifier les journaux du serveur pour l’enregistrement des outils

Erreurs d’exécution

  1. Ajouter une gestion complète des erreurs
  2. Valider les paramètres avant le traitement
  3. Retourner des messages d'erreur faciles à comprendre
  4. Erreurs de journalisation pour le débogage

Problèmes de typage

  1. Utiliser des modèles Pydantic pour les types complexes
  2. Convertir des modèles en dictes avant sérialisation
  3. Gérer explicitement les conversions de types

Prochaines étapes

Ressources additionnelles

Les serveurs AG-UI en Go peuvent laisser les appels d’outils au frontend en désactivant l’appel automatique de fonctions sur l’agent hébergé.

a := foundryprovider.NewAgent(endpoint, token, foundryprovider.ModelDeployment(model), foundryprovider.AgentConfig{
    Instructions: "You are a helpful assistant.",
    Config: agent.Config{
        Name:                "AGUIAssistant",
        DisableFuncAutoCall: true,
    },
})

mux := http.NewServeMux()
mux.Handle("/", aguiprovider.NewJSONHTTPHandler(a, aguiprovider.HandlerConfig{}))

Tip

Consultez l’exemple des outils front-end d’AG-UI pour obtenir un exemple complet exécutable.