Definir ferramentas

As ferramentas dão ao agente no dispositivo a capacidade de acessar dados do app, se comunicar com serviços de back-end e realizar ações em nome do usuário.

Como as ferramentas funcionam

Quando um usuário envia um comando, a classe AgentRunner executa um loop do agente em quatro etapas:

  1. Observação: o Gemini Nano analisa o comando do usuário e o contexto da conversa atual em relação às declarações de ferramentas registradas (o espaço de ação do agente).
  2. Motivo: o modelo determina se uma ferramenta no app é necessária. Se uma ferramenta for necessária, o Gemini Nano vai emitir uma invocação de ferramenta estruturada contendo o nome da função e os argumentos digitados.
  3. Ação: a instância AgentRunner intercepta automaticamente essa invocação e executa a função Kotlin correspondente no app.
  4. Avaliar o feedback: a saída da ferramenta é enviada de volta ao Gemini Nano. O modelo avalia o feedback e responde ao usuário ou chama outra ferramenta até que uma condição de encerramento seja atendida (conclusão da meta ou alcance do limite de maxTurns).

Ferramentas declarativas com Android AppFunctions

A maneira recomendada de definir ferramentas é de forma declarativa usando as AppFunctions do Android (androidx.appfunctions). Elas oferecem um modelo de plataforma padronizado para declarar recursos tipados no app com geração automática de esquema.

1. Adicionar dependências do Gradle

Adicione os artefatos do AppFunctions com genai-agent no arquivo build.gradle.kts do seu módulo:

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. Adicionar a anotação @AppFunctionDeclaration aos métodos de ferramenta

Anote os métodos de ferramenta públicos com a anotação @AppFunctionDeclaration(isDescribedByKDoc = true). As descrições de parâmetros são documentadas em tags @param padrão do KDoc.

As AppFunctions exigem que os valores de retorno sejam tipos primitivos (como String, Int ou Boolean) ou classes de dados estruturados anotadas com a anotação @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."
            }
        )
    }
}

Registre o serviço gerado no arquivo AndroidManifest.xml do app:

<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. Restringir o acesso do autor da chamada com a constante ACCESS_LEVEL_SELF

As AppFunctions não precisam ser públicas para o sistema ou para assistentes externos. No Android, o controle de acesso é gerenciado por níveis de acesso do autor da chamada, em vez de verificações manuais de identidade.

Por padrão (usando a constante ACCESS_LEVEL_ANDROID_TRUSTED), uma AppFunction pode ser descoberta e executada por assistentes no dispositivo certificados pela plataforma que tenham a permissão do sistema EXECUTE_APP_FUNCTIONS. No entanto, ao criar recursos destinados estritamente ao agente interno do app (como inspecionar carrinhos de compras do usuário, acessar tokens locais ou realizar ações no app), é possível restringir o acesso exclusivamente ao seu aplicativo usando a constante 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."
        )
    }
}

A plataforma oferece três níveis de acesso para os autores de chamadas:

  • ACCESS_LEVEL_SELF (100): estritamente restrito ao próprio aplicativo host. Assistentes externos, plataformas do sistema e outros apps não podem descobrir ou invocar a função por IPC. Use esse nível para operações ou ferramentas sensíveis que devem atender apenas ao agente interno do app.
  • ACCESS_LEVEL_SYSTEM (200): restrito a autores de chamada do sistema privilegiados que têm a permissão android.Manifest.permission#EXECUTE_APP_FUNCTIONS_SYSTEM (como a tela de início do sistema ou os apps recentes) e o próprio app host.
  • ACCESS_LEVEL_ANDROID_TRUSTED (300, padrão): aberto para agentes no dispositivo certificados pela plataforma que têm a permissão EXECUTE_APP_FUNCTIONS, as plataformas do sistema e o app host.

4. Registrar ferramentas com o agente

É possível registrar AppFunctions com o agente de forma automática usando o método enableAppFunctions() ou de forma programática usando o método AppFunctionAgentTools.discover():

Descoberta automática com o método enableAppFunctions()

Para apps simples em que as AppFunctions são executadas automaticamente quando o modelo as chama:

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)

Se quiser, chame o método addAppFunctionIds("fetch_order_details", "cancel_order") para limitar a descoberta estritamente a IDs de AppFunction específicos, em vez de expor todas as funções no app.

Descoberta programática com o método AppFunctionAgentTools.discover()

Quando o app precisa inspecionar as AppFunctions descobertas, filtrá-las ou decorá-las antes de registrar com o agente:

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)

Padrão de decorador e human in the loop

Operações sensíveis, como cancelamentos de pedidos, pagamentos ou exclusão de dados do usuário, precisam manter um human in the loop (HITL), exigindo a confirmação do usuário. Todas as ferramentas adicionadas à classe Agent são originadas do Android AppFunctions, e a classe AgentTool fornece um construtor de delegação AgentTool(delegate) para que você possa encapsular as ferramentas AppFunction descobertas usando o padrão 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)
    }
}

Práticas recomendadas para design de ferramentas

  • Defina o escopo do espaço de ação: mantenha o espaço de ação do agente (o conjunto de ferramentas e permissões expostas ao agente) apenas o suficiente para a tarefa usando o método addAppFunctionIds() e a constante ACCESS_LEVEL_SELF. Um espaço de ação excessivamente grande torna um modelo no dispositivo mais propenso a erros e consome tokens de janela de contexto.
  • Seja explícito nas descrições: o Gemini Nano depende das descrições de ferramentas e parâmetros para decidir quando chamar uma ferramenta. Seja claro sobre a finalidade da ferramenta e mencione os formatos de argumento esperados (como datas ou IDs).
  • Retornar mapas de erros estruturados: retorne mapas de erros descritivos, como mapOf("error" to "Order not found"), quando ocorrer uma falha esperada. Isso permite que o Gemini Nano explique o problema de forma clara para o usuário. Se uma exceção inesperada for gerada por uma ferramenta, o ambiente de execução vai capturá-la automaticamente e refletir a mensagem de erro de volta ao modelo para autocorreção.
  • Mantenha os payloads de retorno focados: os modelos no dispositivo têm janelas de contexto menores do que os modelos de servidor. Retorne apenas os campos relevantes para responder à consulta do usuário, em vez de despejar linhas inteiras do banco de dados ou respostas de rede brutas.
  • Executar E/S no agente Dispatchers.IO: sempre garanta que as solicitações de rede e as consultas do banco de dados do Room nas ferramentas sejam executadas no agente Dispatchers.IO para evitar o bloqueio do autor da chamada da corrotina.

Próximas etapas