Поделиться через


Визуализация интерфейсного инструмента с помощью AG-UI

В этом руководстве показано, как добавить инструменты для работы с интерфейсом в клиенты AG-UI. Интерфейсные инструменты — это функции, которые выполняются на стороне клиента, позволяя агенту ИИ взаимодействовать с локальной средой пользователя, получать доступ к данным клиента или выполнять операции пользовательского интерфейса. Сервер определяет, когда вызывать эти средства, но выполнение полностью происходит на стороне клиента.

Предпосылки

Прежде чем приступить к работе, убедитесь, что вы выполнили руководство по началу работы и выполните следующие действия.

  • .NET 8.0 или более поздней версии
  • Microsoft.Agents.AI.AGUI установленный пакет
  • Microsoft.Agents.AI установленный пакет
  • Основные сведения о настройке клиента AG-UI

Что такое интерфейсные инструменты?

Интерфейсные инструменты — это средства функций, которые:

  • Определены и зарегистрированы на клиентской стороне
  • Выполнение в среде клиента (не на сервере)
  • Разрешить агенту ИИ взаимодействовать с ресурсами, зависящими от клиента
  • Предоставьте результаты обратно на сервер для включения агента в ответы
  • Обеспечивайте персонализированный, контекстно-ориентированный опыт

Распространенные варианты использования:

  • Чтение данных локального датчика (GPS, температура и т. д.)
  • Доступ к клиентскому хранилищу или предпочтениям
  • Выполнение операций пользовательского интерфейса (изменение тем, отображение уведомлений)
  • Взаимодействие с функциями, зависящими от устройства (камера, микрофон)

Регистрация интерфейсных средств на клиенте

Основным отличием от учебника по началу работы является регистрация инструментов у клиента-агента. Ниже приведены изменения.

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

Остальная часть клиентского кода остается той же, что и в руководстве по началу работы.

Как инструменты отправляются на сервер

При регистрации инструментов в AsAIAgent(), AGUIChatClient автоматически:

  1. Записывает определения инструментов (имена, описания, схемы параметров)
  2. Отправляет инструменты при каждом запросе агенту сервера, который сопоставляет их с ChatAgentRunOptions.ChatOptions.Tools

Сервер получает объявления клиентского инструмента, а модель искусственного интеллекта может решить, когда их вызывать.

Проверка и изменение средств с использованием промежуточного слоя

Можно использовать посредническое программное обеспечение агента для проверки или изменения его запуска, с доступом к инструментам:

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

Это паттерн промежуточного программного обеспечения позволяет:

  • Проверка определений инструментов перед выполнением

Основные понятия

Ниже приведены новые понятия для интерфейсных инструментов:

  • Регистрация на стороне клиента: средства регистрируются на клиенте с помощью AIFunctionFactory.Create() и передаются в AsAIAgent()
  • Автоматическая запись: средства автоматически записываются и отправляются с помощью ChatAgentRunOptions.ChatOptions.Tools

Как работают интерфейсные инструменты

серверный поток

Сервер не знает сведения о реализации интерфейсных средств. Он знает только:

  1. Имена и описания инструментов (из регистрации клиента)
  2. Схемы параметров
  3. Когда запрашивать выполнение средства

Когда агент ИИ решает вызвать интерфейсное средство:

  1. Сервер отправляет запрос на вызов средства клиенту через SSE
  2. Сервер ожидает, пока клиент выполнит средство и возвращает результаты.
  3. Сервер включает результаты в контекст агента
  4. Агент продолжает обработку с результатами инструмента

поток клиентской стороны

Клиент обрабатывает выполнение интерфейсного инструмента.

  1. FunctionCallContent Получает от сервера сообщение, содержащее запрос на вызов инструмента
  2. Соотносит имя инструмента с локально зарегистрированной функцией.
  3. Десериализует параметры из запроса
  4. Выполняет функцию локально
  5. Сериализация результата
  6. Отправляет FunctionResultContent обратно на сервер
  7. Продолжает получать ответы агента

Ожидаемые выходные данные с фронтенд инструментами

Когда агент вызывает интерфейсные инструменты, вы увидите вызов инструмента и получите результат потоковой передачи.

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.

Настройка сервера для инструментов внешнего интерфейса

Сервер не нуждается в специальной конфигурации для поддержки интерфейсных средств. Используйте типовой сервер AG-UI из учебника для начинающих — он автоматически:

  • Получает объявления фронтенд-инструмента во время подключения клиента
  • Запрашивает выполнение инструмента, когда агент искусственного интеллекта нуждается в нем.
  • Ожидает результатов от клиента
  • Включает результаты в принятие решений агента

Дальнейшие шаги

Теперь, когда вы понимаете интерфейсные инструменты, вы можете:

Дополнительные ресурсы

В этом руководстве показано, как добавить инструменты для работы с интерфейсом в клиенты AG-UI. Интерфейсные инструменты — это функции, которые выполняются на стороне клиента, позволяя агенту ИИ взаимодействовать с локальной средой пользователя, получать доступ к данным клиента или выполнять операции пользовательского интерфейса.

Предпосылки

Прежде чем приступить к работе, убедитесь, что вы выполнили руководство по началу работы и выполните следующие действия.

  • Python 3.10 или более поздней версии
  • httpx установлен для функциональных возможностей КЛИЕНТА HTTP
  • Основные сведения о настройке клиента AG-UI
  • Служба Azure OpenAI настроена

Что такое интерфейсные инструменты?

Интерфейсные инструменты — это средства функций, которые:

  • Определены и зарегистрированы на клиентской стороне
  • Выполнение в среде клиента (не на сервере)
  • Разрешить агенту ИИ взаимодействовать с ресурсами, зависящими от клиента
  • Предоставьте результаты обратно на сервер для включения агента в ответы

Распространенные варианты использования:

  • Чтение данных локального датчика
  • Доступ к клиентскому хранилищу или предпочтениям
  • Выполнение операций пользовательского интерфейса
  • Взаимодействие с функциями, зависящими от устройства

Создание интерфейсных инструментов

Инструменты интерфейса в Python определяются аналогично инструментам бэкенда, но регистрируются с клиентом:

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

Создание клиента AG-UI с помощью интерфейсных средств

Вот полная реализация клиента с интерфейсными инструментами:

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

Как работают интерфейсные инструменты

Поток протокола

  1. Регистрация клиента: клиент отправляет объявления инструментов (имена, описания, параметры) на сервер
  2. Оркестрация сервера: агент ИИ решает, когда следует вызывать интерфейсные инструменты на основе запроса пользователя
  3. Запрос вызова средства: сервер отправляет TOOL_CALL_REQUEST событие клиенту через SSE
  4. Выполнение клиента: клиент выполняет средство локально
  5. Отправка результатов: клиент отправляет результат обратно на сервер с помощью запроса POST
  6. Обработка агента: сервер включает результат и продолжает ответ

Ключевые события

  • TOOL_CALL_REQUEST: сервер запрашивает выполнение внешнего средства
  • TOOL_CALL_RESULT: клиент отправляет результат выполнения (через HTTP POST)

Ожидаемые выходные данные

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]

Настройка сервера

Стандартный сервер AG-UI из руководства по началу работы автоматически поддерживает интерфейсные инструменты. Изменения на стороне сервера не требуются, он автоматически обрабатывает оркестрацию инструментов.

Лучшие практики

Безопасность

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"

Обработка ошибок

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

Асинхронные операции

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

Устранение неполадок

Средства, которые не вызываются

  1. Убедитесь, что декларации инструментов отправляются на сервер
  2. Описание инструментов должно четко указывать их назначение.
  3. Проверка журналов сервера для регистрации средства

Ошибки выполнения

  1. Добавление комплексной обработки ошибок
  2. Проверка параметров перед обработкой
  3. Возврат сообщений об ошибках, понятных для пользователя
  4. Журналировать ошибки для отладки

Проблемы с типами данных

  1. Использование моделей Pydantic для сложных типов
  2. Преобразование моделей в словари перед сериализацией
  3. Обрабатывайте преобразования типов явно

Дальнейшие шаги

Дополнительные ресурсы