Rendering degli strumenti Front-end con AG-UI

Gli strumenti front-end vengono dichiarati ed eseguiti dal client AG-UI. Il server riceve gli schemi in modo che il modello possa richiederli, ma non riceve le implementazioni.

Registrare uno strumento front-end

Creare lo strumento e passarlo all'agente supportato da 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 gestisce il flusso di continuazione:

  1. Invia la dichiarazione dello strumento front-end con la richiesta di esecuzione.
  2. Riceve la chiamata dello strumento del modello dal server.
  3. Esegue la funzione corrispondente in locale.
  4. Invia di nuovo il risultato al server.
  5. Continua l'esecuzione e invia in streaming la risposta finale.

Suggerimento

Vedere l'esempio di .NET front-end-tools per un client e un server completi.

Avvertimento

Le dichiarazioni e i risultati degli strumenti forniti da un client non attendibile sono input non attendibili. Autorizzare gli strumenti client che possono influenzare l'esecuzione dell'agente sul lato server e convalidare i risultati prima di usarli per le operazioni con privilegi.

Per indicazioni generali sulla creazione di strumenti, vedere Usare gli strumenti per le funzioni con un agente.

Passaggi successivi

Questa esercitazione illustra come aggiungere strumenti di funzione front-end ai client AG-UI. Gli strumenti front-end sono funzioni eseguite sul lato client, consentendo all'agente di intelligenza artificiale di interagire con l'ambiente locale dell'utente, accedere ai dati specifici del client o eseguire operazioni dell'interfaccia utente.

Prerequisiti

Prima di iniziare, assicurarsi di aver completato l'esercitazione introduttiva e di avere:

  • Python 3.10 o versione successiva
  • httpx installato per la funzionalità client HTTP
  • Conoscenza di base della configurazione del client di AG-UI
  • Servizio OpenAI di Azure configurato

Che cosa sono gli strumenti front-end?

Gli strumenti front-end sono strumenti per le funzioni che:

  • Vengono definiti e registrati nel client
  • Eseguire nell'ambiente del client (non nel server)
  • Consentire all'agente di intelligenza artificiale di interagire con risorse specifiche del client
  • Fornire i risultati al server affinché l'agente possa incorporarli nelle risposte

Casi d'uso comuni:

  • Lettura dei dati dei sensori locali
  • Accesso all'archiviazione sul lato client o alle preferenze
  • Esecuzione di operazioni dell'interfaccia utente
  • Interazione con funzionalità specifiche del dispositivo

Creazione di strumenti front-end

Gli strumenti front-end in Python sono definiti in modo analogo agli strumenti back-end, ma vengono registrati con il 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}"

Creazione di un client AG-UI con gli strumenti front-end

Ecco un'implementazione client completa con gli strumenti 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())

Funzionamento degli strumenti front-end

Flusso del protocollo

  1. Registrazione del client: il client invia dichiarazioni degli strumenti (nomi, descrizioni, parametri) al server
  2. Orchestrazione server: l'agente di intelligenza artificiale decide quando chiamare gli strumenti front-end in base alla richiesta dell'utente
  3. Eventi di invocazione di strumenti: il server trasmette al client gli eventi TOOL_CALL_START, TOOL_CALL_ARGS e TOOL_CALL_END
  4. Esecuzione client: il client esegue lo strumento in locale
  5. Eventi risultato: i risultati degli strumenti vengono rappresentati come TOOL_CALL_RESULT eventi nel flusso
  6. Elaborazione agente: il server incorpora il risultato e continua la risposta

Eventi chiave

  • TOOL_CALL_START / TOOL_CALL_ARGS / TOOL_CALL_END: Il server richiede e trasmette in streaming i dettagli delle chiamate agli strumenti
  • TOOL_CALL_RESULT: evento del risultato dell'esecuzione dello strumento

Output previsto

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]

Installazione del server

Il server AG-UI standard dell'esercitazione introduttiva supporta automaticamente gli strumenti front-end. Nessuna modifica necessaria sul lato server: gestisce automaticamente l'orchestrazione degli strumenti.

Migliori pratiche

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"

Gestione degli errori

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

Operazioni asincrone

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

Troubleshooting

Strumenti non chiamati

  1. Verificare che le dichiarazioni degli strumenti vengano inviate al server
  2. Verificare che le descrizioni degli strumenti indichino chiaramente il scopo
  3. Controllare i log del server per la registrazione dello strumento

Errori di esecuzione

  1. Aggiungere una gestione completa degli errori
  2. Convalidare i parametri prima dell'elaborazione
  3. Restituire messaggi di errore intuitivi
  4. Errori di log per il debug

Problemi di tipo

  1. Usare modelli Pydantic per tipi complessi
  2. Convertire i modelli in dict prima della serializzazione
  3. Gestire le conversioni dei tipi in modo esplicito

Operazioni successive

Risorse aggiuntive

I server Go AG-UI possono lasciare le chiamate agli strumenti al frontend disabilitando la chiamata automatica delle funzioni sull'agente ospitato.

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

Suggerimento

Consulta l'esempio degli strumenti frontend di AG-UI per un esempio completo e funzionante.