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:
- 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).
- 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.
- Agisci: l'istanza
AgentRunnerintercetta automaticamente questa chiamata ed esegue la funzione Kotlin corrispondente nella tua app. - 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'autorizzazioneandroid.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'autorizzazioneEXECUTE_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 costanteACCESS_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 dispatcherDispatchers.IOper evitare di bloccare il chiamante della coroutine.
Passaggi successivi
- Scopri come mantenere il contesto della conversazione durante le rotazioni delle app e ripulire le sessioni in Gestisci le sessioni e il ciclo di vita.
- Gestisci i token di testo in streaming e gli stati di esecuzione degli strumenti in Eventi di streaming e integrazione della UI.