Créer des workflows agentifs avec Gemini Nano

Grâce aux workflows agentiques sur l'appareil de ML Kit (com.google.mlkit:genai-agent, disponibles via le programme de preview développeur AICore), vous pouvez créer des agents de conversation autonomes et multitours optimisés par Gemini Nano et le service système Android AICore.

Basée sur la classe GenerativeModel, qui fournit un accès direct au modèle, la classe AgentRunner automatise les boucles agentiques complètes : planification d'actions en plusieurs étapes, appel d'outils typés (comme l'interrogation d'un dépôt d'applications, l'appel d'une API de backend ou la lecture d'une base de données locale), évaluation des résultats des outils et gestion du contexte conversationnel localement sur l'appareil, sans latence réseau et dans le respect total de la confidentialité de l'utilisateur. Bien que la classe GenerativeModel soit compatible avec l'historique multitour géré par le client à l'aide d'un objet List<Content> (avec les rôles USER et MODEL) et d'une valeur sessionId, la classe AgentRunner fournit des sessions multitours gérées (à l'aide de l'interface AgentSession) qui enregistrent automatiquement l'historique des conversations en mémoire d'un tour à l'autre, de sorte que vous n'avez pas à gérer les transcriptions manuellement. Les déclarations d'outils statiques et les instructions système sont également mises en cache automatiquement sur l'appareil, ce qui réduit la latence du délai avant le premier jeton sans configuration supplémentaire.

Composants principaux

  • La classe Agent : déclare les instructions, les outils disponibles et les garde-fous d'exécution à l'aide de la méthode Agent.builder(). La valeur par défaut est maxTurns = 10 automatiquement.
  • La classe AgentRunner : basée sur la classe GenerativeModel, elle orchestre les boucles de raisonnement autonomes et les sessions gérées avec des modèles sur l'appareil. Main-safe par défaut sur Dispatchers.IO, avec temperature défini par défaut sur null (qui utilise la valeur par défaut du modèle ; transmettez temperature = 0.0f pour le décodage glouton).
  • L'interface AgentSession : handle de session de conversation multitour limité obtenu à l'aide de la méthode runner.getOrCreateSession(sessionId) qui enregistre automatiquement l'historique des conversations en mémoire entre les tours, avec les méthodes runAsync(content) et close().
  • L'interface AgentEvent : flux Kotlin Flow réactif émettant des jetons de texte et des mises à jour d'outils.
  • La classe AgentTool : fonctions typées pouvant être appelées par le modèle, définies à l'aide d'AppFunctions Android (@AppFunctionDeclaration).
  • Classe GenAiException : hiérarchie d'exceptions typées (com.google.mlkit.genai.common.GenAiException) signalant des états d'échec tels que MAX_TURNS_EXCEEDED, NOT_AVAILABLE ou BUSY.

Prérequis et dépendances

Avant d'intégrer l'API Agent, assurez-vous que votre environnement de développement et votre configuration Gradle répondent aux exigences suivantes.

Exigences liées aux plates-formes et au matériel

Configuration de Gradle

Ajoutez la dépendance genai-agent au fichier build.gradle.kts de votre module :

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")
}

Bien que genai-agent ne nécessite pas de processeur d'annotation ni de plug-in de compilateur, la définition d'outils avec androidx.appfunctions nécessite le plug-in KSP appfunctions-compiler.

Guide de démarrage rapide : Initialiser un agent

L'exemple de code suivant montre comment instancier un agent et un exécuteur sur l'appareil :

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)

Définir les outils

Le modèle peut appeler des outils typés pour interroger les données de l'application, communiquer avec les services de backend ou déclencher des actions dans l'application. Les outils sont définis de manière déclarative à l'aide des AppFunctions Android (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",
        )
    }
}

Enregistrez le service généré dans le fichier AndroidManifest.xml de votre application :

<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>

Restreindre l'accès des appelants avec la constante ACCESS_LEVEL_SELF

Les AppFunctions n'ont pas besoin d'être publiques pour le système ou les appelants externes. Dans Android, le contrôle des accès est géré via les niveaux d'accès de l'appelant plutôt qu'en inspectant les identités des appelants.

Par défaut (à l'aide de la constante ACCESS_LEVEL_ANDROID_TRUSTED), les AppFunctions peuvent être appelées par des assistants sur l'appareil certifiés par la plate-forme et disposant de l'autorisation système EXECUTE_APP_FUNCTIONS. Si votre outil est strictement destiné à l'agent interne de votre application (par exemple, pour gérer les paniers des utilisateurs ou les enregistrements de base de données privés), limitez l'accès des appelants à l'aide de 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() {

    /**
     * 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."
    }
}

Lorsqu'elle est limitée par la constante ACCESS_LEVEL_SELF, la plate-forme empêche les surfaces externes et les assistants système de découvrir ou d'exécuter la fonction via l'IPC.

Configurer l'agent

Configurez une instance Agent avec des instructions système et activez AppFunctions, puis initialisez l'instance 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)

Vous pouvez également appeler la méthode addAppFunctionIds("fetch_order_details", "cancel_order") pour limiter la découverte à des ID AppFunction spécifiques au lieu d'exposer toutes les fonctions intégrées à l'application.

Paramètres par défaut de l'agent

  • Limites de tours : la classe Agent.Builder utilise par défaut maxTurns = 10 automatiquement comme condition d'arrêt pour éviter les boucles d'appel d'outils infinies. Vous n'avez pas besoin d'appeler manuellement la méthode setMaxTurns(10) pour les workflows standards de conversation ou d'appel d'outils.
  • Décodage glouton déterministe : la classe AgentRunner définit le paramètre temperature par défaut sur null, en se reportant à la valeur par défaut du modèle sous-jacent. Pour une sélection déterministe des outils et la conformité au schéma JSON sur l'appareil, transmettez temperature = 0.0f (temperature) au constructeur AgentRunner. Évitez de définir des températures de style cloud (> 0,4f), qui augmentent les risques d'hallucination et de dégradation du schéma sur les modèles mobiles.

Exécution multitour et événements de streaming

Obtenez une instance AgentSession à l'aide de la méthode runner.getOrCreateSession(sessionId) dans une classe ViewModel Android. La session renvoie un flux Flow<AgentEvent> qui émet des jetons de texte en temps réel et des mises à jour d'exécution des outils :

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()
    }
}

Diffuser en continu des jetons de texte directement

Pour les interfaces de conversation où les détails d'exécution de l'outil n'ont pas besoin d'être affichés séparément dans l'interface utilisateur, utilisez la fonction d'extension textTokens() sur l'interface 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)
    }

Exécution en un seul tour

Pour les tâches ponctuelles où l'historique des dialogues ne doit pas être conservé en RAM (par exemple, l'ajout de tags aux photos ou la synthèse de texte éphémère), appelez directement la méthode 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)
    }

Lorsque vous appelez directement la méthode runner.runSingleTurnAsync(content), l'instance AgentRunner alloue un ID de session éphémère en interne et supprime automatiquement l'état de la conversation de la mémoire une fois l'opération terminée, ce qui évite les fuites de mémoire.

Cycle de vie de la session et gestion de la mémoire

L'historique des dialogues multitours est stocké en mémoire et indexé par sa valeur sessionId :

  • Modifications de configuration : conserver l'instance AgentSession dans une classe Android ViewModel permet de préserver automatiquement l'historique des conversations lors des rotations d'écran, sans avoir à gérer les chaînes à chaque rotation.
  • Éviction de session : appelez la méthode session.delete() pour évincer une session de manière synchrone lorsqu'un fil de conversation est fermé, ou appelez la méthode session.close() sans suspension dans la méthode ViewModel.onCleared() pour libérer de la RAM.
  • Éviction groupée : appelez la méthode runner.deleteSession(sessionId) pour libérer une session spécifique ou la méthode runner.clearAllSessions() pour supprimer toutes les conversations mises en cache.
  • Cycle de vie de l'exécuteur : la classe AgentRunner implémente l'interface AutoCloseable. Lorsque vous partagez un exécuteur sur plusieurs écrans, fermez les poignées AgentSession individuelles dans la méthode ViewModel.onCleared() de chaque écran, puis appelez la méthode runner.close() lorsque vous arrêtez complètement l'exécuteur.
  • Sessions simultanées : une seule instance AgentRunner peut héberger plusieurs sessions distinctes simultanément. Chaque valeur sessionId conserve un historique de conversation isolé.

Gestion des exceptions avec la classe GenAiException

Les échecs d'exécution lors de l'exécution de l'agent, par exemple lorsque l'agent atteint le nombre maximal de tours d'outil sans générer de réponse ou lorsque l'IA sur l'appareil est temporairement indisponible, sont émis ou générés en tant qu'exception GenAiException (com.google.mlkit.genai.common.GenAiException). Gérez les échecs de manière réactive à l'aide de l'opérateur catch de flux de coroutines Kotlin :

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
    }

Principaux codes d'erreur

  • GenAiException.ErrorCode.MAX_TURNS_EXCEEDED (-112) : l'agent a dépassé la limite maximale de tours (la limite maxTurns) sans générer de réponse finale.
  • GenAiException.ErrorCode.NOT_AVAILABLE (8) : le modèle Gemini Nano ou le service système AICore n'est pas disponible ou prêt.
  • GenAiException.ErrorCode.BUSY (9) : le système est occupé ou l'application appelante a dépassé son quota.