Definir herramientas

Las herramientas le brindan a tu agente integrado en el dispositivo la capacidad de acceder a los datos de la app, comunicarse con los servicios de backend y realizar acciones en nombre del usuario.

Cómo funcionan las herramientas

Cuando un usuario envía una instrucción, la clase AgentRunner ejecuta un bucle de agente en cuatro etapas:

  1. Observar: Gemini Nano analiza la instrucción del usuario y el contexto de la conversación actual en función de las declaraciones de herramientas registradas (el espacio de acción del agente).
  2. Motivo: El modelo determina si se requiere una herramienta integrada en la app. Si se necesita una herramienta, Gemini Nano emite una invocación de herramienta estructurada que contiene el nombre de la función y los argumentos con tipo.
  3. Act: La instancia de AgentRunner intercepta automáticamente esta invocación y ejecuta la función de Kotlin correspondiente en tu app.
  4. Evalúa los comentarios: El resultado de la herramienta se vuelve a enviar a Gemini Nano. El modelo evalúa los comentarios y responde al usuario o llama a otra herramienta hasta que se cumple una condición de finalización (completar el objetivo o alcanzar el límite de maxTurns).

Herramientas declarativas con AppFunctions de Android

La forma recomendada de definir herramientas es de forma declarativa con AppFunctions de Android (androidx.appfunctions). AppFunctions proporciona un modelo de plataforma estandarizado para declarar capacidades de la app con tipos con generación automática de esquemas.

1. Agrega dependencias de Gradle

Agrega los artefactos de AppFunctions junto con genai-agent en el archivo build.gradle.kts de tu 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. Anota los métodos de la herramienta con la anotación @AppFunctionDeclaration.

Anota los métodos públicos de la herramienta con la anotación @AppFunctionDeclaration(isDescribedByKDoc = true). Las descripciones de los parámetros se documentan en etiquetas @param de KDoc estándar.

Las AppFunctions requieren que los valores que se muestran sean tipos primitivos (como String, Int o Boolean) o clases de datos estructurados anotadas con la anotación @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."
            }
        )
    }
}

Registra el servicio generado en el archivo AndroidManifest.xml de tu 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. Restringe el acceso de los emisores con la constante ACCESS_LEVEL_SELF

Las AppFunctions no tienen que ser públicas para el sistema ni para los asistentes externos. En Android, el control de acceso se administra a través de los niveles de acceso del emisor, en lugar de las verificaciones manuales de identidad del emisor.

De forma predeterminada (con la constante ACCESS_LEVEL_ANDROID_TRUSTED), los asistentes integrados en el dispositivo certificados por la plataforma que tengan el permiso del sistema EXECUTE_APP_FUNCTIONS pueden descubrir y ejecutar una AppFunction. Sin embargo, cuando creas funciones destinadas estrictamente al agente interno de tu app (como inspeccionar los carritos de compras de los usuarios, acceder a tokens locales o realizar acciones en la app), puedes restringir el acceso exclusivamente a tu propia aplicación con la 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."
        )
    }
}

La plataforma admite tres niveles de acceso de los emisores:

  • ACCESS_LEVEL_SELF (100): Se restringe estrictamente a la aplicación host en sí. Los asistentes externos, las plataformas del sistema y otras apps no pueden descubrir ni invocar la función a través de IPC. Usa este nivel para operaciones o herramientas sensibles que solo deberían entregar el agente interno de tu app.
  • ACCESS_LEVEL_SYSTEM (200): Se restringe a los emisores del sistema con privilegios que tienen el permiso android.Manifest.permission#EXECUTE_APP_FUNCTIONS_SYSTEM (como el Selector del sistema o Recientes) y la app host.
  • ACCESS_LEVEL_ANDROID_TRUSTED (300, predeterminado): Se abre para los agentes integrados en el dispositivo certificados por la plataforma que tienen el permiso EXECUTE_APP_FUNCTIONS, las plataformas del sistema y la app host.

4. Registra herramientas con el agente

Puedes registrar AppFunctions con el agente de forma automática con el método enableAppFunctions() o de forma programática con el método AppFunctionAgentTools.discover():

Descubrimiento automático con el método enableAppFunctions()

Para apps sencillas en las que las AppFunctions se ejecutan automáticamente cuando el modelo las llama, haz lo siguiente:

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)

De manera opcional, llama al método addAppFunctionIds("fetch_order_details", "cancel_order") para limitar el descubrimiento estrictamente a IDs de AppFunction específicos en lugar de exponer todas las funciones de la app.

Descubrimiento programático con el método AppFunctionAgentTools.discover()

Cuando tu app necesite inspeccionar las AppFunctions descubiertas, filtrarlas o decorarlas antes de registrarlas con el agente, haz lo siguiente:

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)

Patrón de decorador y con intervención humana

Las operaciones sensibles, como las cancelaciones de pedidos, los pagos o la eliminación de datos de los usuarios, deben mantener una intervención humana (HITL), ya que requieren la confirmación del usuario. Todas las herramientas que se agregan a la clase Agent se originan en las AppFunctions de Android, y la clase AgentTool proporciona un constructor de delegación AgentTool(delegate) para que puedas unir las herramientas de AppFunction descubiertas con el patrón 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ácticas recomendadas para el diseño de herramientas

  • Define el alcance del espacio de acción: Mantén el espacio de acción del agente (el conjunto de herramientas y permisos expuestos al agente) lo suficientemente grande para su tarea con el método addAppFunctionIds() y la constante ACCESS_LEVEL_SELF. Un espacio de acción demasiado grande hace que un modelo integrado en el dispositivo sea más propenso a errores y consuma tokens de la ventana de contexto.
  • Sé explícito en las descripciones: Gemini Nano se basa en las descripciones de las herramientas y los parámetros para decidir cuándo llamar a una herramienta. Sé claro sobre el propósito de la herramienta y menciona los formatos de argumentos esperados (como fechas o IDs).
  • Devuelve mapas de errores estructurados: Devuelve mapas de errores descriptivos, como mapOf("error" to "Order not found"), cuando se produce una falla esperada. Esto permite que Gemini Nano le explique el problema al usuario de forma clara. Si una herramienta arroja una excepción inesperada, el entorno de ejecución la detecta automáticamente y refleja el mensaje de error en el modelo para la autocorrección.
  • Mantén enfocadas las cargas útiles de devolución: Los modelos integrados en el dispositivo tienen ventanas de contexto más pequeñas que los modelos de servidor. Muestra solo los campos relevantes para responder la consulta del usuario en lugar de volcar filas completas de la base de datos o respuestas de red sin procesar.
  • Ejecuta operaciones de E/S en el despachador Dispatchers.IO: Asegúrate siempre de que las solicitudes de red y las consultas de la base de datos de Room dentro de las herramientas se ejecuten en el despachador Dispatchers.IO para evitar que se bloquee el emisor de la corrutina.

Próximos pasos