Bagikan melalui


Perenderan Alat Frontend dengan AG-UI

Tutorial ini menunjukkan kepada Anda cara menambahkan alat fungsi frontend ke klien AG-UI Anda. Alat frontend adalah fungsi yang dijalankan di sisi klien, memungkinkan agen AI berinteraksi dengan lingkungan lokal pengguna, mengakses data khusus klien, atau melakukan operasi UI. Server mengatur kapan harus memanggil alat-alat ini, tetapi eksekusi terjadi sepenuhnya pada klien.

Prasyarat

Sebelum memulai, pastikan Anda telah menyelesaikan tutorial Memulai dan memiliki:

  • .NET 8.0 atau yang lebih baru
  • Microsoft.Agents.AI.AGUI paket terinstal
  • Microsoft.Agents.AI paket terinstal
  • Pemahaman dasar tentang penyiapan klien AG-UI

Apa itu Alat Frontend?

Alat frontend adalah alat fungsi yang:

  • Didefinisikan dan didaftarkan pada klien
  • Jalankan di lingkungan klien (bukan di server)
  • Mengizinkan agen AI berinteraksi dengan sumber daya khusus klien
  • Kirim kembali hasil ke server agar agen dapat mengintegrasikan ke dalam respons.
  • Mengaktifkan pengalaman yang dipersonalisasi dan sadar konteks

Kasus penggunaan umum:

  • Membaca data sensor lokal (GPS, suhu, dll.)
  • Mengakses penyimpanan atau preferensi sisi klien
  • Melakukan operasi UI (mengubah tema, menampilkan pemberitahuan)
  • Berinteraksi dengan fitur khusus perangkat (kamera, mikrofon)

Mendaftarkan Alat Frontend pada Klien

Perbedaan utama dari tutorial Memulai adalah mendaftarkan alat dengan agen klien. Berikut adalah perubahannya:

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

Sisa kode klien Anda tetap sama seperti yang ditunjukkan dalam tutorial Memulai.

Cara Alat Dikirim ke Server

Saat Anda mendaftarkan alat dengan AsAIAgent(), AGUIChatClient secara otomatis:

  1. Menangkap definisi alat (nama, deskripsi, skema parameter)
  2. Mengirimkan alat bersama setiap permintaan ke agen server yang memetakannya ChatAgentRunOptions.ChatOptions.Tools

Server menerima deklarasi alat klien dan model AI dapat memutuskan kapan harus memanggilnya.

Memeriksa dan Memodifikasi Alat dengan Middleware

Anda dapat menggunakan middleware agen untuk memeriksa atau memodifikasi eksekusi agen, termasuk mengakses alat:

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

Pola middleware ini memungkinkan Anda untuk:

  • Memvalidasi definisi alat sebelum eksekusi

Konsep utama

Berikut ini adalah konsep baru untuk alat frontend:

  • Pendaftaran sisi klien: Alat didaftarkan pada klien menggunakan AIFunctionFactory.Create() dan diteruskan ke AsAIAgent()
  • Pengambilan otomatis: Alat secara otomatis diambil dan dikirim melalui ChatAgentRunOptions.ChatOptions.Tools

Cara Kerja Alat Frontend

Proses Server-Side

Server tidak tahu detail implementasi alat frontend. Hanya mengetahui:

  1. Nama dan deskripsi alat (dari pendaftaran klien)
  2. Skema parameter
  3. Kapan harus meminta eksekusi alat

Ketika agen AI memutuskan untuk memanggil alat frontend:

  1. Server mengirimkan permintaan panggilan alat ke klien melalui SSE
  2. Server menunggu klien untuk menjalankan alat dan mengembalikan hasil
  3. Server mengintegrasikan hasil ke dalam konteks agen
  4. Agen terus memproses dengan hasil dari alat

Proses Sisi Klien

Klien menangani eksekusi alat frontend:

  1. Menerima FunctionCallContent dari server yang menunjukkan permintaan panggilan alat
  2. Mencocokkan nama alat dengan fungsi terdaftar secara lokal
  3. Mendeserialisasi parameter dari permintaan
  4. Menjalankan fungsi secara lokal
  5. Menserialisasikan hasil
  6. FunctionResultContent Mengirim kembali ke server
  7. Terus menerima respons agen

Output yang Diharapkan dengan Alat Frontend

Saat agen memanggil alat frontend, Anda akan melihat panggilan alat dan menghasilkan output 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.

Penyiapan Server untuk Alat Frontend

Server tidak memerlukan konfigurasi khusus untuk mendukung alat frontend. Gunakan server AG-UI standar dari tutorial Memulai Cepat - secara otomatis:

  • Menerima deklarasi alat frontend selama koneksi klien
  • Meminta eksekusi alat saat agen AI membutuhkannya
  • Menunggu hasil dari klien
  • Menggabungkan hasil ke dalam pengambilan keputusan agen

Langkah Selanjutnya

Sekarang setelah Anda memahami alat frontend, Anda dapat:

Sumber Daya Tambahan

Tutorial ini menunjukkan kepada Anda cara menambahkan alat fungsi frontend ke klien AG-UI Anda. Alat frontend adalah fungsi yang dijalankan di sisi klien, memungkinkan agen AI berinteraksi dengan lingkungan lokal pengguna, mengakses data khusus klien, atau melakukan operasi UI.

Prasyarat

Sebelum memulai, pastikan Anda telah menyelesaikan tutorial Memulai dan memiliki:

  • Python 3.10 atau yang lebih baru
  • httpx diinstal untuk fungsionalitas klien HTTP
  • Pemahaman dasar tentang penyiapan klien AG-UI
  • Layanan Azure OpenAI dikonfigurasi

Apa itu Alat Frontend?

Alat frontend adalah alat fungsi yang:

  • Didefinisikan dan didaftarkan pada klien
  • Jalankan di lingkungan klien (bukan di server)
  • Mengizinkan agen AI berinteraksi dengan sumber daya khusus klien
  • Kirim kembali hasil ke server agar agen dapat mengintegrasikan ke dalam respons.

Kasus penggunaan umum:

  • Membaca data sensor lokal
  • Mengakses penyimpanan atau preferensi sisi klien
  • Melakukan operasi UI
  • Berinteraksi dengan fitur khusus perangkat

Membuat Alat Frontend

Alat frontend di Python didefinisikan mirip dengan alat backend tetapi terdaftar di klien:

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

Membuat Klien AG-UI dengan Alat Frontend

Berikut adalah implementasi klien lengkap dengan alat frontend:

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

Cara Kerja Alat Frontend

Sistem Aliran Protokol

  1. Pendaftaran Klien: Klien mengirim deklarasi alat (nama, deskripsi, parameter) ke server
  2. Orkestrasi Server: Agen AI memutuskan kapan harus memanggil alat frontend berdasarkan permintaan pengguna
  3. Permintaan Panggilan Alat: Server mengirim TOOL_CALL_REQUEST event ke klien melalui SSE
  4. Eksekusi Klien: Klien menjalankan alat secara lokal
  5. Pengiriman Hasil: Klien mengirim hasil kembali ke server melalui permintaan POST
  6. Pemrosesan Agen: Server menggabungkan hasil dan melanjutkan respons

Peristiwa Utama

  • TOOL_CALL_REQUEST: Server meminta eksekusi alat frontend
  • TOOL_CALL_RESULT: Klien mengirimkan hasil eksekusi (melalui HTTP POST)

Output yang Diharapkan

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]

Penyetelan Server

Server AG-UI standar dari tutorial Memulai secara otomatis mendukung alat frontend. Tidak ada perubahan yang diperlukan di sisi server - ia menangani orkestrasi alat secara otomatis.

Praktik Terbaik

Keamanan

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"

Penanganan Kesalahan

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

Operasi Asinkron

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

Troubleshooting

Alat Tidak Dipanggil

  1. Pastikan bahwa deklarasi alat dikirim ke server
  2. Memastikan deskripsi alat menunjukkan tujuan dengan jelas
  3. Periksa log server untuk pendaftaran alat

Kesalahan Eksekusi

  1. Menambahkan penanganan kesalahan komprehensif
  2. Memvalidasi parameter sebelum memproses
  3. Mengembalikan pesan kesalahan yang mudah digunakan
  4. Kesalahan pencatatan untuk debugging

Masalah Tipe

  1. Menggunakan model Pydantic untuk jenis kompleks
  2. Mengonversi model menjadi dict sebelum serialisasi
  3. Menangani konversi tipe secara eksplisit

Langkah Selanjutnya

Sumber Daya Tambahan