Crea workflow agentici con Gemini Nano

Con gli Agentic Workflows on-device di ML Kit (com.google.mlkit:genai-agent, disponibili tramite il programma Anteprima per gli sviluppatori di AICore), puoi creare agenti di conversazione autonomi a più turni basati su Gemini Nano e sul servizio di sistema Android AICore.

Basata sulla classe GenerativeModel, che fornisce l'accesso diretto al modello, la classe AgentRunner automatizza i loop degli agenti completi: pianificazione di azioni in più passaggi, richiamo di strumenti tipizzati (come l'esecuzione di query su un repository di app, la chiamata di un'API di backend o la lettura di un database locale), valutazione dei risultati degli strumenti e gestione del contesto di conversazione localmente on-device con latenza di rete pari a zero e privacy dell'utente completa. Sebbene la classe GenerativeModel supporti la cronologia multi-turn gestita dal client utilizzando un oggetto List<Content> (con ruoli USER e MODEL) e un valore sessionId, la classe AgentRunner fornisce sessioni multi-turn gestite (utilizzando l'interfaccia AgentSession) che registrano automaticamente la cronologia delle conversazioni in memoria tra i turni, in modo da non dover gestire manualmente le trascrizioni. Anche le dichiarazioni degli strumenti statici e le istruzioni di sistema vengono memorizzate automaticamente nella cache on-device, riducendo la latenza del tempo al primo token senza configurazioni aggiuntive.

Componenti principali

  • La classe Agent: dichiara istruzioni, strumenti disponibili e guardrail di esecuzione utilizzando il metodo Agent.builder(). Il valore predefinito è maxTurns = 10 automaticamente.
  • La classe AgentRunner: basata sulla classe GenerativeModel, orchestra i cicli di ragionamento autonomi e le sessioni gestite con i modelli on-device. Main-safe per impostazione predefinita su Dispatchers.IO, con temperature impostato su null (che rimanda al valore predefinito del modello; passa temperature = 0.0f per la decodifica greedy).
  • L'interfaccia AgentSession: handle della sessione di conversazione multi-turno con ambito ottenuto utilizzando il metodo runner.getOrCreateSession(sessionId) che registra automaticamente la cronologia delle conversazioni in memoria tra i turni, con i metodi runAsync(content) e close().
  • L'interfaccia AgentEvent: lo stream Kotlin Flow reattivo che emette token di testo e aggiornamenti degli strumenti.
  • La classe AgentTool: funzioni tipizzate richiamabili dal modello, definite utilizzando AppFunctions di Android (@AppFunctionDeclaration).
  • La GenAiException classe: gerarchia di eccezioni tipizzate (com.google.mlkit.genai.common.GenAiException) che segnala stati di errore come MAX_TURNS_EXCEEDED, NOT_AVAILABLE o BUSY.

Prerequisiti e dipendenze

Prima di integrare l'API Agent, assicurati che l'ambiente di sviluppo e la configurazione di Gradle soddisfino i seguenti requisiti.

Requisiti della piattaforma e dell'hardware

Configurazione di Gradle

Aggiungi la dipendenza genai-agent al file build.gradle.kts del tuo modulo:

dependencies {
    // Gemini Nano agentic workflows
    implementation("com.google.mlkit:genai-agent:1.0.0-alpha1")

    // Optional: Only required when defining tools using Android AppFunctions
    implementation("androidx.appfunctions:appfunctions:1.0.0-alpha13")
    ksp("androidx.appfunctions:appfunctions-compiler:1.0.0-alpha13")
}

Sebbene genai-agent non richieda un processore di annotazioni o un plug-in del compilatore, la definizione degli strumenti con androidx.appfunctions richiede il plug-in KSP appfunctions-compiler.

Guida rapida: inizializza un agente

Il seguente esempio di codice mostra come creare un'istanza di un runner e di un agente on-device:

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

// Configure the agent with system instructions
val agent = Agent.builder()
    .setInstructions("You are a helpful on-device assistant.")
    .build()

// Create the runner to execute prompts
val runner = AgentRunner(agent)

Definisci gli strumenti

Il modello può chiamare strumenti tipizzati per eseguire query sui dati dell'app, comunicare con i servizi di backend o attivare azioni in-app. Gli strumenti vengono definiti in modo dichiarativo utilizzando Android AppFunctions (androidx.appfunctions):

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

@AppFunctionSerializable
data class OrderDetails(
    val orderId: String,
    val status: String,
    val eta: String,
)

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

    /**
     * Fetches details and delivery status for an in-app order.
     *
     * @param orderId The alphanumeric order identifier
     */
    @AppFunctionDeclaration(isDescribedByKDoc = true)
    suspend fun fetch_order_details(
        orderId: String
    ): OrderDetails = withContext(Dispatchers.IO) {
        // Query local database or call your backend API
        OrderDetails(
            orderId = orderId,
            status = "Out for delivery",
            eta = "2:30 PM",
        )
    }
}

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>

Limita l'accesso del chiamante con la costante ACCESS_LEVEL_SELF

Le AppFunction non devono essere pubbliche per il sistema o per i chiamanti esterni. In Android, il controllo degli accessi viene gestito tramite i livelli di accesso del chiamante anziché ispezionando le identità dei chiamanti.

Per impostazione predefinita (utilizzando la costante ACCESS_LEVEL_ANDROID_TRUSTED), le AppFunction possono essere chiamate dagli assistenti on-device certificati dalla piattaforma che dispongono dell'autorizzazione di sistema EXECUTE_APP_FUNCTIONS. Se lo strumento è destinato esclusivamente all'agente interno della tua app (ad esempio, per la gestione dei carrelli degli utenti o dei record di database privati), limita l'accesso del chiamante 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() {

    /**
     * Cancels an order. Restricted strictly to this app's internal agent.
     */
    @OptIn(ExperimentalAppFunctionsApi::class)
    @AppFunctionDeclaration(isDescribedByKDoc = true)
    @AppFunctionAccessLevel(
        level = AppFunctionMetadata.ACCESS_LEVEL_SELF,
        isCompatEnforcementEnabled = true,
    )
    fun cancel_order(
        orderId: String
    ): String {
        // Only your app's internal agent can invoke this function
        return "Order $orderId cancelled successfully."
    }
}

Se limitata con la costante ACCESS_LEVEL_SELF, la piattaforma impedisce alle piattaforme esterne e agli assistenti di sistema di rilevare o eseguire la funzione tramite IPC.

Configura l'agente

Configura un'istanza Agent con le istruzioni di sistema e abilita AppFunctions, quindi inizializza l'istanza AgentRunner:

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

// Configure the agent with system instructions and enable AppFunctions
val agent = Agent.builder()
    .setName("OrderAssistant")
    .setInstructions(
        "You are a helpful assistant for looking up store orders. " +
        "Use available tools to fetch order information."
    )
    .enableAppFunctions(true) // Automatically discovers all registered AppFunctions in your app
    .build()

// Initialize the runner
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.

Impostazioni predefinite dell'agente

  • Limiti di turni: la classe Agent.Builder imposta automaticamente maxTurns = 10 come condizione di terminazione per evitare loop infiniti di chiamate agli strumenti. Non è necessario chiamare manualmente il metodo setMaxTurns(10) per i workflow standard di conversazione o di chiamata allo strumento.
  • Decodifica greedy deterministica: la classe AgentRunner imposta il parametro temperature predefinito su null, rimandando al valore predefinito del modello sottostante. Per la selezione deterministica degli strumenti e la conformità allo schema JSON on-device, passa temperature = 0.0f (temperature) al costruttore AgentRunner. Evita di impostare temperature in stile cloud (> 0,4f), che aumentano i rischi di allucinazioni e di degradazione dello schema sui modelli mobile.

Esecuzione in più turni ed eventi di streaming

Ottieni un'istanza AgentSession utilizzando il metodo runner.getOrCreateSession(sessionId) all'interno di una classe ViewModel Android. La sessione restituisce uno stream Flow<AgentEvent> che emette token di testo in tempo reale e aggiornamenti sull'esecuzione degli strumenti:

package com.example.ai.agent

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.mlkit.genai.agent.AgentEvent
import com.google.mlkit.genai.agent.AgentRunner
import com.google.mlkit.genai.agent.AgentSession
import com.google.mlkit.genai.common.GenAiException
import com.google.mlkit.genai.prompt.Content
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch

data class AgentUiState(
    val messages: List<String> = emptyList(),
    val currentResponse: String = "",
    val activeToolStatus: String? = null,
    val isLoading: Boolean = false,
    val errorMessage: String? = null
)

class ChatViewModel(
    private val runner: AgentRunner,
    private val sessionId: String = "chat_session"
) : ViewModel() {

    // Scoped session preserves conversation history across configuration changes (screen rotations)
    private val session: AgentSession = runner.getOrCreateSession(sessionId)

    private val _uiState = MutableStateFlow(AgentUiState())
    val uiState: StateFlow<AgentUiState> = _uiState.asStateFlow()

    fun sendMessage(userText: String) {
        _uiState.update {
            it.copy(
                messages = it.messages + userText,
                currentResponse = "",
                activeToolStatus = null,
                isLoading = true,
                errorMessage = null
            )
        }

        viewModelScope.launch {
            val content = Content.builder().text(userText).build()
            session.runAsync(content)
                .catch { e ->
                    val message = when {
                        e is GenAiException && e.errorCode == GenAiException.ErrorCode.MAX_TURNS_EXCEEDED ->
                            "Agent exceeded the maximum reasoning turns allowed."
                        e is GenAiException && e.errorCode == GenAiException.ErrorCode.NOT_AVAILABLE ->
                            "On-device AI is currently unavailable."
                        e is GenAiException && e.errorCode == GenAiException.ErrorCode.BUSY ->
                            "On-device AI is busy. Please try again."
                        else -> e.localizedMessage ?: "Agent error"
                    }
                    _uiState.update {
                        it.copy(
                            isLoading = false,
                            activeToolStatus = null,
                            errorMessage = message
                        )
                    }
                }
                .collect { event ->
                    when (event) {
                        is AgentEvent.TextMessage -> {
                            _uiState.update {
                                it.copy(
                                    currentResponse = if (event.isPartial) {
                                        it.currentResponse + event.text
                                    } else {
                                        event.text.ifEmpty { it.currentResponse }
                                    },
                                    activeToolStatus = null
                                )
                            }
                        }
                        is AgentEvent.FunctionCall -> {
                            _uiState.update {
                                it.copy(activeToolStatus = "Executing ${event.name}...")
                            }
                        }
                        is AgentEvent.FunctionResponse -> {
                            _uiState.update {
                                it.copy(activeToolStatus = "Received data from ${event.name}")
                            }
                        }
                        else -> {}
                    }
                }

            // Finalize message once stream completes
            _uiState.update {
                it.copy(
                    messages = it.messages + it.currentResponse,
                    currentResponse = "",
                    activeToolStatus = null,
                    isLoading = false
                )
            }
        }
    }

    override fun onCleared() {
        super.onCleared()
        // Cleanly evicts conversation memory from RAM when ViewModel is destroyed
        session.close()
    }
}

Trasmetti in streaming i token di testo direttamente

Per le interfacce conversazionali in cui i dettagli di esecuzione dello strumento non devono essere visualizzati separatamente nell'interfaccia utente, utilizza la funzione di estensione textTokens() nell'interfaccia Flow<AgentEvent>:

import com.google.mlkit.genai.agent.textTokens
import com.google.mlkit.genai.prompt.Content

val content = Content.builder().text("What is my order status?").build()
session.runAsync(content)
    .textTokens()
    .collect { token ->
        print(token)
    }

Esecuzione a turno singolo

Per le attività una tantum in cui la cronologia della conversazione non deve essere conservata nella RAM (ad esempio, il tagging delle foto o la sintesi di testo temporaneo), chiama direttamente il metodo runner.runSingleTurnAsync(content):

// Ephemeral single-turn: cleans up session state from RAM automatically upon completion
val content = Content.builder()
    .text("Summarize the order history for customer #1042: ...")
    .build()

runner.runSingleTurnAsync(content)
    .textTokens()
    .collect { token ->
        displaySummary(token)
    }

Quando richiami direttamente il metodo runner.runSingleTurnAsync(content), l'istanza AgentRunner alloca internamente un ID sessione temporaneo ed elimina automaticamente lo stato della conversazione dalla memoria al termine, evitando perdite di memoria.

Ciclo di vita della sessione e gestione della memoria

La cronologia dei dialoghi multi-turn viene archiviata in memoria e indicizzata in base al valore sessionId:

  • Modifiche alla configurazione: la conservazione dell'istanza AgentSession all'interno di una classe ViewModel Android conserva automaticamente la cronologia delle conversazioni durante le rotazioni dello schermo senza dover inserire stringhe a ogni turno.
  • Espulsione della sessione: chiama il metodo session.delete() per espellere in modo sincrono una sessione quando un thread di conversazione viene ignorato oppure chiama il metodo session.close() non sospeso nel metodo ViewModel.onCleared() per liberare la RAM.
  • Espulsione collettiva: chiama il metodo runner.deleteSession(sessionId) per liberare una sessione specifica o il metodo runner.clearAllSessions() per espellere tutte le conversazioni memorizzate nella cache.
  • Ciclo di vita del runner: la classe AgentRunner implementa l'interfaccia AutoCloseable. Quando condividi un runner tra più schermate, chiudi i singoli handle AgentSession nel metodo ViewModel.onCleared() di ogni schermata e chiama il metodo runner.close() quando arresti completamente il runner.
  • Sessioni simultanee: una singola istanza AgentRunner può ospitare più sessioni distinte contemporaneamente. Ogni valore sessionId mantiene una cronologia delle conversazioni isolata.

Gestione degli errori con la classe GenAiException

Gli errori di runtime durante l'esecuzione dell'agente, ad esempio quando l'agente raggiunge il numero massimo di turni dello strumento senza generare una risposta o quando l'AI on-device non è temporaneamente disponibile, vengono emessi o generati come eccezione GenAiException (com.google.mlkit.genai.common.GenAiException). Gestisci gli errori in modo reattivo utilizzando l'operatore catch di Kotlin Coroutines Flow:

session.runAsync(content)
    .catch { e ->
        if (e is GenAiException) {
            when (e.errorCode) {
                GenAiException.ErrorCode.MAX_TURNS_EXCEEDED -> {
                    // Turn limit exceeded
                }
                GenAiException.ErrorCode.NOT_AVAILABLE -> {
                    // Gemini Nano or AICore unavailable
                }
                GenAiException.ErrorCode.BUSY -> {
                    // System busy or rate limit reached
                }
                else -> {
                    // Other GenAiException errors
                }
            }
        }
    }
    .collect { event ->
        // Process response events
    }

Codici di errore principali

  • GenAiException.ErrorCode.MAX_TURNS_EXCEEDED (-112): l'agente ha superato il limite massimo di turni (il limite maxTurns) senza generare una risposta finale.
  • GenAiException.ErrorCode.NOT_AVAILABLE (8): il modello Gemini Nano o il servizio di sistema AICore non è disponibile o pronto.
  • GenAiException.ErrorCode.BUSY (9): il sistema è occupato o l'app di chiamate ha superato la sua quota.