Condividi tramite


Rendering degli strumenti Front-end con AG-UI

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. Il server orchestra quando chiamare questi strumenti, ma l'esecuzione avviene interamente sul client.

Prerequisiti

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

  • .NET 8.0 o versione successiva
  • Microsoft.Agents.AI.AGUI pacchetto installato
  • Microsoft.Agents.AI pacchetto installato
  • Conoscenza di base della configurazione del client di AG-UI

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
  • Abilitare esperienze personalizzate e con riconoscimento del contesto

Casi d'uso comuni:

  • Lettura dei dati dei sensori locali (GPS, temperatura e così via)
  • Accesso all'archiviazione sul lato client o alle preferenze
  • Esecuzione di operazioni dell'interfaccia utente (modifica dei temi, visualizzazione delle notifiche)
  • Interazione con le funzionalità specifiche del dispositivo (fotocamera, microfono)

Registrazione degli strumenti front-end nel client

La differenza principale rispetto all'esercitazione introduttiva consiste nel registrare gli strumenti con l'agente client. Ecco le modifiche apportate:

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

Il resto del codice client rimane invariato come illustrato nell'esercitazione introduttiva.

Modalità di invio degli strumenti al server

Quando si registrano gli strumenti con AsAIAgent(), il AGUIChatClient lo fa automaticamente.

  1. Acquisisce le definizioni degli strumenti (nomi, descrizioni, schemi di parametri)
  2. Invia gli strumenti con ogni richiesta all'agente server, che esegue il mapping su ChatAgentRunOptions.ChatOptions.Tools

Il server riceve le dichiarazioni degli strumenti client e il modello di intelligenza artificiale può decidere quando chiamarli.

Controllo e modifica di strumenti con middleware

È possibile usare il middleware agente per controllare o modificare l'esecuzione dell'agente, incluso l'accesso agli strumenti:

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

Questo modello middleware consente di:

  • Convalidare le definizioni degli strumenti prima dell'esecuzione

Concetti chiave

Di seguito sono riportati i nuovi concetti per gli strumenti front-end:

  • Registrazione sul lato client: gli strumenti vengono registrati nel client usando AIFunctionFactory.Create() e passati a AsAIAgent()
  • Acquisizione automatica: gli strumenti vengono acquisiti e inviati automaticamente tramite ChatAgentRunOptions.ChatOptions.Tools

Funzionamento degli strumenti front-end

Flusso lato server

Il server non conosce i dettagli di implementazione degli strumenti front-end. Lo sa solo:

  1. Nomi e descrizioni degli strumenti (dalla registrazione client)
  2. Schemi dei parametri
  3. Quando richiedere l'esecuzione dello strumento

Quando l'agente di intelligenza artificiale decide di chiamare uno strumento front-end:

  1. Il server invia una richiesta di chiamata dello strumento al client tramite SSE
  2. Il server attende l'esecuzione dello strumento da parte del client e restituisce i risultati
  3. Il server incorpora i risultati nel contesto dell'agente
  4. L'agente continua l'elaborazione con i risultati dello strumento

flusso lato client

Il client gestisce l'esecuzione dello strumento front-end:

  1. Riceve FunctionCallContent dal server che indica una richiesta di chiamata dello strumento
  2. Trova la corrispondenza tra il nome dello strumento e una funzione registrata localmente
  3. Deserializza i parametri dalla richiesta
  4. Esegue la funzione in locale
  5. Serializza il risultato
  6. Invia FunctionResultContent di nuovo al server
  7. Continua a ricevere risposte dell'agente

Output previsto con gli strumenti front-end

Quando l'agente chiama gli strumenti front-end, viene visualizzata la chiamata allo strumento e viene restituito l'output di streaming:

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.

Installazione del server per gli strumenti front-end

Il server non richiede una configurazione speciale per supportare gli strumenti front-end. Usare il server AG-UI standard della guida introduttiva - si avvia automaticamente:

  • Riceve le dichiarazioni degli strumenti front-end durante la connessione client
  • Richiede l'esecuzione dello strumento quando l'agente di intelligenza artificiale ne necessita
  • Attende i risultati dal client
  • Incorpora i risultati nel processo decisionale dell'agente

Passaggi successivi

Dopo aver compreso gli strumenti front-end, è possibile:

Risorse aggiuntive

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)

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

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. Richiesta di chiamata allo strumento: il server invia TOOL_CALL_REQUEST l'evento al client tramite SSE
  4. Esecuzione client: il client esegue lo strumento in locale
  5. Invio di risultati: il client invia il risultato al server tramite richiesta POST
  6. Elaborazione agente: il server incorpora il risultato e continua la risposta

Eventi chiave

  • TOOL_CALL_REQUEST: il server richiede l'esecuzione dello strumento front-end
  • TOOL_CALL_RESULT: il client invia il risultato dell'esecuzione (tramite HTTP POST)

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"

Risoluzione dei problemi

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

Passaggi successivi

Risorse aggiuntive