Definire gli strumenti

Gli strumenti consentono all'agente sul dispositivo di accedere ai dati dell'app, comunicare con i servizi di backend ed eseguire azioni per conto dell'utente.

Come funzionano gli strumenti

Quando un utente invia un prompt, la classe AgentRunner esegue un loop dell'agente in quattro fasi:

  1. Osservazione: Gemini Nano analizza il prompt dell'utente e il contesto della conversazione corrente in base alle dichiarazioni degli strumenti registrati (lo spazio di azione dell'agente).
  2. Motivo: il modello determina se è necessario uno strumento in-app. Se è necessario uno strumento, Gemini Nano genera una chiamata strutturata dello strumento contenente il nome della funzione e gli argomenti tipizzati.
  3. Agisci: l'istanza AgentRunner intercetta automaticamente questa chiamata ed esegue la funzione Kotlin corrispondente nella tua app.
  4. Valutazione del feedback: l'output dello strumento viene inviato nuovamente a Gemini Nano. Il modello valuta il feedback e risponde all'utente o chiama un altro strumento finché non viene soddisfatta una condizione di interruzione (completamento dell'obiettivo o raggiungimento del limite di maxTurns).

Strumenti dichiarativi con AppFunctions di Android

Il modo consigliato per definire gli strumenti è in modo dichiarativo utilizzando le AppFunctions di Android (androidx.appfunctions). Le AppFunctions forniscono un modello di piattaforma standardizzato per dichiarare le funzionalità in-app tipizzate con la generazione automatica dello schema.

1. Aggiungi dipendenze Gradle

Aggiungi gli artefatti AppFunctions insieme a genai-agent nel file build.gradle.kts del modulo:

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. Annota i metodi degli strumenti con l'annotazione @AppFunctionDeclaration

Annota i metodi degli strumenti pubblici con l'annotazione @AppFunctionDeclaration(isDescribedByKDoc = true). Le descrizioni dei parametri sono documentate nei tag KDoc @param standard.

AppFunctions richiede che i valori restituiti siano tipi primitivi (come String, Int o Boolean) o classi di dati strutturati annotate con l'annotazione @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 il servizio generato nel file AndroidManifest.xml dell'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. Limita l'accesso del chiamante con la costante ACCESS_LEVEL_SELF

Le AppFunction non devono essere pubbliche per il sistema o per gli assistenti esterni. In Android, il controllo degli accessi viene gestito tramite i livelli di accesso del chiamante anziché tramite i controlli manuali dell'identità del chiamante.

Per impostazione predefinita (utilizzando la costante ACCESS_LEVEL_ANDROID_TRUSTED), un'AppFunction può essere rilevata ed eseguita dagli assistenti on-device certificati dalla piattaforma che dispongono dell'autorizzazione di sistema EXECUTE_APP_FUNCTIONS. Tuttavia, quando crei funzionalità destinate esclusivamente all'agente interno della tua app (ad esempio l'ispezione dei carrelli degli acquisti degli utenti, l'accesso ai token locali o l'esecuzione di azioni in-app), puoi limitare l'accesso esclusivamente alla tua applicazione utilizzando la costante 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 piattaforma supporta tre livelli di accesso per i chiamanti:

  • ACCESS_LEVEL_SELF (100): limitato rigorosamente all'applicazione host stessa. Gli assistenti esterni, le piattaforme di sistema e altre app non possono rilevare o richiamare la funzione tramite IPC. Utilizza questo livello per operazioni o strumenti sensibili che devono essere utilizzati solo dall'agente interno della tua app.
  • ACCESS_LEVEL_SYSTEM (200): limitato ai chiamanti di sistema con privilegi che dispongono dell'autorizzazione android.Manifest.permission#EXECUTE_APP_FUNCTIONS_SYSTEM (come Avvio app di sistema o Recenti) e all'app host stessa.
  • ACCESS_LEVEL_ANDROID_TRUSTED (300, predefinito): aperto agli agenti on-device certificati dalla piattaforma con l'autorizzazione EXECUTE_APP_FUNCTIONS, alle piattaforme di sistema e all'app host.

4. Registra gli strumenti con l'agente

Puoi registrare le AppFunction con l'agente automaticamente utilizzando il metodo enableAppFunctions() o in modo programmatico utilizzando il metodo AppFunctionAgentTools.discover():

Rilevamento automatico con il metodo enableAppFunctions()

Per app semplici in cui le AppFunction vengono eseguite automaticamente quando il modello le chiama:

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)

Facoltativamente, chiama il metodo addAppFunctionIds("fetch_order_details", "cancel_order") per limitare il rilevamento esclusivamente a ID AppFunction specifici anziché esporre tutte le funzioni in-app.

Rilevamento programmatico con il metodo AppFunctionAgentTools.discover()

Quando l'app deve ispezionare le AppFunction rilevate, filtrarle o decorarle prima di registrarle con l'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)

Pattern Decorator e human-in-the-loop

Le operazioni sensibili, come l'annullamento di ordini, i pagamenti o l'eliminazione di dati utente, devono mantenere un approccio human-in-the-loop (HITL) richiedendo la conferma dell'utente. Tutti gli strumenti aggiunti alla classe Agent provengono da AppFunctions di Android e la classe AgentTool fornisce un costruttore di delega AgentTool(delegate) in modo da poter eseguire il wrapping degli strumenti AppFunction rilevati utilizzando il pattern 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)
    }
}

Best practice per la progettazione degli strumenti

  • Definisci l'ambito dello spazio di azione: mantieni lo spazio di azione dell'agente (l'insieme di strumenti e autorizzazioni esposti all'agente) sufficientemente ampio per la sua attività utilizzando il metodo addAppFunctionIds() e la costante ACCESS_LEVEL_SELF. Uno spazio di azione eccessivamente ampio rende un modello on-device più soggetto a errori e consuma token della finestra contestuale.
  • Fornisci descrizioni esplicite: Gemini Nano si basa sulle descrizioni degli strumenti e dei parametri per decidere quando chiamare uno strumento. Specifica chiaramente lo scopo dello strumento e menziona i formati degli argomenti previsti (ad esempio date o ID).
  • Restituisci mappe di errori strutturate: restituisci mappe di errori descrittive come mapOf("error" to "Order not found") quando si verifica un errore previsto. In questo modo, Gemini Nano può spiegare il problema all'utente in modo chiaro. Se uno strumento genera un'eccezione imprevista, il runtime la rileva automaticamente e restituisce il messaggio di errore al modello per l'autocorrezione.
  • Mantieni i payload di ritorno mirati: i modelli on-device hanno finestre di contesto più piccole rispetto ai modelli server. Restituisci solo i campi pertinenti per rispondere alla query dell'utente, anziché scaricare intere righe del database o risposte di rete non elaborate.
  • Esegui l'I/O sul dispatcher Dispatchers.IO: assicurati sempre che le richieste di rete e le query del database Room all'interno degli strumenti vengano eseguite sul dispatcher Dispatchers.IO per evitare di bloccare il chiamante della coroutine.

Passaggi successivi