Определите инструменты

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

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

Когда пользователь отправляет запрос, класс AgentRunner выполняет цикл агента, состоящий из четырех этапов:

  1. Наблюдение. Gemini Nano анализирует запрос пользователя и текущий контекст разговора, сопоставляя их с зарегистрированными объявлениями инструментов (пространством действий агента).
  2. Причина. Модель определяет, требуется ли инструмент в приложении. Если инструмент необходим, Gemini Nano выдает структурированный вызов инструмента, содержащий имя функции и типизированные аргументы.
  3. Действие. Экземпляр AgentRunner автоматически перехватывает этот вызов и выполняет соответствующую функцию Kotlin в вашем приложении.
  4. Оценка отзыва. Результат работы инструмента передается обратно в Gemini Nano. Модель оценивает отзыв и либо отвечает пользователю, либо вызывает другой инструмент, пока не будет выполнено условие завершения (достижение цели или достижение лимита maxTurns).

Декларативные инструменты с Android AppFunctions

Рекомендуемый способ определения инструментов – декларативный, с использованием Android AppFunctions (androidx.appfunctions). AppFunctions предоставляет стандартизированную модель платформы для объявления типизированных возможностей приложения с автоматическим созданием схемы.

1. Добавьте зависимости Gradle

Добавьте артефакты AppFunctions вместе с genai-agent в файл build.gradle.kts модуля:

dependencies {
    implementation("com.google.mlkit:genai-agent:1.0.0-alpha1")
    implementation("androidx.appfunctions:appfunctions:1.0.0-alpha13")
    ksp("androidx.appfunctions:appfunctions-compiler:1.0.0-alpha13")
}

2. Добавьте к методам инструментов аннотацию @AppFunctionDeclaration.

Добавьте к методам общедоступных инструментов аннотацию @AppFunctionDeclaration(isDescribedByKDoc = true). Описания параметров задокументированы в стандартных тегах KDoc @param.

AppFunctions требуют, чтобы возвращаемые значения были либо примитивными типами (такими как String, Int или Boolean), либо классами структурированных данных, аннотированными с помощью аннотации @AppFunctionSerializable.

package com.example.ai.agent.tools

import androidx.appfunctions.AppFunctionDeclaration
import androidx.appfunctions.AppFunctionSerializable
import androidx.appfunctions.AppFunctionService
import androidx.appfunctions.AppFunctionServiceEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

/**
 * Structured details for an order.
 */
@AppFunctionSerializable
data class OrderDetails(
    val orderId: String,
    val status: String,
    val estimatedDelivery: String?,
    val itemCount: Int,
)

/**
 * Result returned when cancelling an order.
 */
@AppFunctionSerializable
data class CancelOrderResult(
    val orderId: String,
    val cancelled: Boolean,
    val message: String,
)

@AppFunctionServiceEntryPoint(
    serviceName = "OrderAppFunctionService",
    appFunctionXmlFileName = "order_app_functions",
)
abstract class OrderTools : AppFunctionService() {

    private val orderRepository = OrderRepository()

    /**
     * Fetches details and delivery status for a customer order by ID.
     *
     * @param orderId The alphanumeric order identifier (for example, ORD-8492)
     */
    @AppFunctionDeclaration(isDescribedByKDoc = true)
    suspend fun fetch_order_details(
        orderId: String
    ): OrderDetails = withContext(Dispatchers.IO) {
        val order = orderRepository.findOrder(orderId)
        if (order != null) {
            OrderDetails(
                orderId = order.id,
                status = order.status,
                estimatedDelivery = order.deliveryEstimate,
                itemCount = order.items.size,
            )
        } else {
            OrderDetails(
                orderId = orderId,
                status = "NOT_FOUND",
                estimatedDelivery = null,
                itemCount = 0,
            )
        }
    }

    /**
     * Requests a cancellation or return for an active order.
     *
     * @param orderId The order identifier to cancel
     * @param reason The reason provided by the user for cancellation
     */
    @AppFunctionDeclaration(isDescribedByKDoc = true)
    suspend fun cancel_order(
        orderId: String,
        reason: String? = null
    ): CancelOrderResult = withContext(Dispatchers.IO) {
        val success = orderRepository.cancelOrder(orderId, reason)
        CancelOrderResult(
            orderId = orderId,
            cancelled = success,
            message = if (success) {
                "Order cancellation processed."
            } else {
                "Order cannot be cancelled at this stage."
            }
        )
    }
}

Зарегистрируйте сгенерированный сервис в файле AndroidManifest.xml приложения:

<service
    android:name=".OrderAppFunctionService"
    android:permission="android.permission.BIND_APP_FUNCTION_SERVICE"
    android:exported="true">
    <property
        android:name="android.app.appfunctions.schema"
        android:value="app_functions_schema.xsd" />
    <property
        android:name="android.app.appfunctions.v2"
        android:value="order_app_functions.xml" />
    <intent-filter>
        <action android:name="android.app.appfunctions.AppFunctionService" />
    </intent-filter>
</service>

3. Ограничение доступа вызывающего абонента с помощью константы ACCESS_LEVEL_SELF

AppFunctions не обязательно должны быть общедоступными для системы или внешних помощников. В Android контроль доступа осуществляется с помощью уровней доступа вызывающего абонента, а не ручной проверки его идентификационных данных.

По умолчанию (с использованием константы ACCESS_LEVEL_ANDROID_TRUSTED) AppFunction может быть обнаружена и выполнена сертифицированными платформой помощниками на устройстве, у которых есть системное разрешение EXECUTE_APP_FUNCTIONS. Однако если вы создаете функции, предназначенные исключительно для внутреннего агента вашего приложения (например, для проверки корзин пользователей, доступа к локальным токенам или выполнения действий в приложении), вы можете ограничить доступ только своим приложением, используя константу ACCESS_LEVEL_SELF:

import androidx.appfunctions.AppFunctionAccessLevel
import androidx.appfunctions.AppFunctionDeclaration
import androidx.appfunctions.AppFunctionService
import androidx.appfunctions.AppFunctionServiceEntryPoint
import androidx.appfunctions.ExperimentalAppFunctionsApi
import androidx.appfunctions.metadata.AppFunctionMetadata

@AppFunctionServiceEntryPoint(
    serviceName = "OrderAppFunctionService",
    appFunctionXmlFileName = "order_app_functions",
)
abstract class OrderTools : AppFunctionService() {

    private val orderRepository = OrderRepository()

    /**
     * Cancels an order. Restricted exclusively to this app's internal agent.
     */
    @OptIn(ExperimentalAppFunctionsApi::class)
    @AppFunctionDeclaration(isDescribedByKDoc = true)
    @AppFunctionAccessLevel(
        level = AppFunctionMetadata.ACCESS_LEVEL_SELF,
        isCompatEnforcementEnabled = true,
    )
    suspend fun cancel_order(orderId: String): CancelOrderResult {
        // Only your app's internal agent can invoke this function
        val success = orderRepository.cancelOrder(orderId)
        return CancelOrderResult(
            orderId = orderId,
            cancelled = success,
            message = if (success) "Cancelled." else "Failed."
        )
    }
}

Платформа поддерживает три уровня доступа для звонящих:

  • ACCESS_LEVEL_SELF (100). Строго ограничено самим хост-приложением. Сторонние помощники, системные интерфейсы и другие приложения не могут обнаруживать или вызывать функцию через IPC. Используйте этот уровень для конфиденциальных операций или инструментов, которые должны обслуживать только внутреннего агента вашего приложения.
  • ACCESS_LEVEL_SYSTEM (200). Доступно только для системных вызывающих абонентов с разрешением android.Manifest.permission#EXECUTE_APP_FUNCTIONS_SYSTEM (например, системного средства запуска или списка недавно использованных приложений) и самого хост-приложения.
  • ACCESS_LEVEL_ANDROID_TRUSTED (300, по умолчанию). Доступно для сертифицированных платформой агентов на устройстве с разрешением EXECUTE_APP_FUNCTIONS, системных интерфейсов и хост-приложения.

4. Как зарегистрировать инструменты в агенте

Зарегистрировать AppFunctions в агенте можно автоматически с помощью метода enableAppFunctions() или программно с помощью метода AppFunctionAgentTools.discover().

Автоматическое обнаружение с помощью метода enableAppFunctions()

Для простых приложений, в которых AppFunctions выполняются автоматически, когда модель вызывает их:

import com.google.mlkit.genai.agent.Agent
import com.google.mlkit.genai.agent.AgentRunner

val agent = Agent.builder()
    .setName("OrderAssistant")
    .setInstructions("Assist users with order tracking.")
    .enableAppFunctions(true) // Automatically discovers all AppFunctions in your app package
    .setMaxTurns(10)
    .build()

val runner = AgentRunner(agent)

При необходимости вызовите метод addAppFunctionIds("fetch_order_details", "cancel_order"), чтобы ограничить поиск определенными идентификаторами AppFunction, а не всеми функциями приложения.

Автоматическое обнаружение с помощью метода AppFunctionAgentTools.discover()

Если приложению нужно проверить обнаруженные AppFunctions, отфильтровать или декорировать их перед регистрацией в агенте:

import com.google.mlkit.genai.agent.Agent
import com.google.mlkit.genai.agent.AgentRunner
import com.google.mlkit.genai.agent.AppFunctionAgentTools

val discoveredTools = AppFunctionAgentTools.discover(
    enabledAppFunctionIds = listOf("fetch_order_details", "cancel_order") // Optional ID filter
)

val agent = Agent.builder()
    .setName("OrderAssistant")
    .setInstructions("Assist users with order tracking.")
    .addTools(discoveredTools)
    .build()

val runner = AgentRunner(agent)

Шаблон "Декоратор" и человек в контуре

Для выполнения конфиденциальных операций, таких как отмена заказа, оплата или удаление пользовательских данных, требуется подтверждение пользователя. Это называется человек в контуре. Все инструменты, добавленные в класс Agent, происходят из Android AppFunctions, а класс AgentTool предоставляет делегирующий конструктор AgentTool(delegate), чтобы вы могли обернуть обнаруженные инструменты AppFunction с помощью шаблона Decorator:

import com.google.mlkit.genai.agent.AgentTool

class HitlConfirmationTool(
    delegate: AgentTool,
    private val onConfirm: suspend (toolName: String, args: Map<String, Any>) -> Boolean
) : AgentTool(delegate) {

    override suspend fun execute(args: Map<String, Any>): Map<String, Any?> {
        val confirmed = onConfirm(name, args)
        if (!confirmed) {
            return mapOf(
                "status" to "CANCELLED_BY_USER",
                "message" to "Action was declined by the user."
            )
        }
        return checkNotNull(delegate).execute(args)
    }
}

Рекомендации по разработке инструментов

  • Ограничьте пространство действий. Пространство действий агента (набор инструментов и разрешений, доступных ему) должно быть достаточным для выполнения задачи. Для этого используйте метод addAppFunctionIds() и константу ACCESS_LEVEL_SELF. Слишком большое пространство действий повышает вероятность ошибок локальной модели и расходует токены окна контекста.
  • Будьте точны в описаниях. Gemini Nano использует описания инструментов и параметров, чтобы решать, когда вызывать инструмент. Четко опишите назначение инструмента и укажите ожидаемые форматы аргументов (например, даты или идентификаторы).
  • Возвращать структурированные карты ошибок. Возвращать описательные карты ошибок, например mapOf("error" to "Order not found"), когда происходит ожидаемый сбой. Это позволяет Gemini Nano корректно объяснить пользователю, в чем проблема. Если инструмент выдает непредвиденное исключение, среда выполнения автоматически перехватывает его и возвращает сообщение об ошибке модели для самокоррекции.
  • Ограничивайте объем возвращаемых данных. У моделей на устройстве окна контекста меньше, чем у серверных моделей. Возвращайте только те поля, которые имеют отношение к ответу на запрос пользователя, а не все строки базы данных или необработанные ответы сети.
  • Выполняйте операции ввода-вывода в диспетчере Dispatchers.IO. Сетевые запросы и запросы к базе данных Room в инструментах должны выполняться в диспетчере Dispatchers.IO, чтобы не блокировать вызывающий объект сопрограммы.

Дальнейшие действия