Crea flujos de trabajo de agentes con Gemini Nano

Con los flujos de trabajo de agentes integrados en el dispositivo del ML Kit (com.google.mlkit:genai-agent, disponibles a través del programa de versión preliminar para desarrolladores de AICore), puedes crear agentes conversacionales autónomos de varios turnos con la tecnología de Gemini Nano y el servicio del sistema Android AICore.

Creada sobre la clase GenerativeModel, que proporciona acceso directo al modelo, la clase AgentRunner automatiza bucles de agente completos: planifica acciones de varios pasos, invoca herramientas con tipo (como consultar un repositorio de apps, llamar a una API de backend o leer una base de datos local), evalúa los resultados de las herramientas y administra el contexto de la conversación de forma integrado en el dispositivo con cero latencia de red y total privacidad del usuario. Mientras que la clase GenerativeModel admite el historial de varios turnos administrado por el cliente con un objeto List<Content> (con roles USER y MODEL) y un valor sessionId, la clase AgentRunner proporciona sesiones de varios turnos administradas (con la interfaz AgentSession) que registran el historial de conversaciones en la memoria automáticamente en todos los turnos para que no tengas que administrar las transcripciones de forma manual. Las declaraciones de herramientas estáticas y las instrucciones del sistema también se almacenan en caché automáticamente integrado en el dispositivo, lo que reduce la latencia del tiempo hasta el primer token sin configuración adicional.

Componentes centrales

  • La clase Agent: Declara instrucciones, herramientas disponibles y protecciones de ejecución con el método Agent.builder(). Se establece de forma predeterminada en maxTurns = 10 automáticamente.
  • La clase AgentRunner: Se basa en la clase GenerativeModel y organiza bucles de razonamiento autónomos y sesiones administradas con modelos integrados en el dispositivo. Seguro para el subproceso principal de forma predeterminada en Dispatchers.IO, con temperature de forma predeterminada en null (que se ajusta al valor predeterminado del modelo; pasa temperature = 0.0f para la decodificación voraz).
  • La interfaz AgentSession: Es el controlador de sesión de conversación de varios turnos con alcance que se obtiene con el método runner.getOrCreateSession(sessionId) que registra automáticamente el historial de conversaciones en la memoria en todos los turnos, con los métodos runAsync(content) y close().
  • La interfaz AgentEvent: El flujo reactivo de Kotlin Flow que emite tokens de texto y actualizaciones de herramientas.
  • La clase AgentTool: Funciones con tipo que puede llamar el modelo, definidas con AppFunctions de Android (@AppFunctionDeclaration).
  • La clase GenAiException: Es la jerarquía de excepciones con tipo (com.google.mlkit.genai.common.GenAiException) que indica estados de falla, como MAX_TURNS_EXCEEDED, NOT_AVAILABLE o BUSY.

Requisitos previos y dependencias

Asegúrate de que tu entorno de desarrollo y la configuración de Gradle cumplan con los siguientes requisitos antes de integrar la API de Agent.

Requisitos de plataforma y hardware

Configuración de Gradle

Agrega la dependencia genai-agent al archivo build.gradle.kts de tu módulo:

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

Si bien genai-agent en sí no requiere un procesador de anotaciones ni un complemento de compilador, definir herramientas con androidx.appfunctions requiere el complemento de KSP de appfunctions-compiler.

Guía de inicio rápido: Inicializa un agente

En la siguiente muestra de código, se indica cómo crear una instancia de un agente y un ejecutor integrados en el dispositivo:

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)

Define herramientas

El modelo puede llamar a herramientas con tipo para consultar datos de la app, comunicarse con servicios de backend o activar acciones en la app. Las herramientas se definen de forma declarativa con las AppFunctions de 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",
        )
    }
}

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>

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 emisores externos. En Android, el control de acceso se controla a través de los niveles de acceso del emisor, en lugar de inspeccionar las identidades 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 llamar a las AppFunctions. Si tu herramienta está destinada estrictamente al agente interno de tu app (por ejemplo, para administrar carritos de usuarios o registros de bases de datos privadas), restringe el acceso de los emisores 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() {

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

Cuando se restringe con la constante ACCESS_LEVEL_SELF, la plataforma impide que las plataformas externas y los asistentes del sistema descubran o ejecuten la función a través de IPC.

Configura el agente

Configura una instancia de Agent con instrucciones del sistema y habilita AppFunctions. Luego, inicializa la instancia de 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)

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.

Configuración predeterminada del agente

  • Límites de turnos: La clase Agent.Builder establece de forma predeterminada maxTurns = 10 automáticamente como una condición de finalización para evitar bucles infinitos de llamadas a herramientas. No es necesario que llames al método setMaxTurns(10) de forma manual para los flujos de trabajo estándar de conversación o de llamada a herramientas.
  • Decodificación codiciosa determinística: La clase AgentRunner establece el parámetro predeterminado temperature en null, lo que se difiere al valor predeterminado del modelo subyacente. Para la selección determinística de herramientas y el cumplimiento del esquema JSON integrado en el dispositivo, pasa temperature = 0.0f (temperature) al constructor AgentRunner. Evita establecer temperaturas de estilo de nube (> 0.4f), que aumentan los riesgos de alucinación y la degradación del esquema en los modelos para dispositivos móviles.

Ejecución de varios turnos y eventos de transmisión

Obtén una instancia de AgentSession con el método runner.getOrCreateSession(sessionId) dentro de una clase ViewModel de Android. La sesión devuelve un flujo de Flow<AgentEvent> que emite tokens de texto en tiempo real y actualizaciones de ejecución de herramientas:

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

Transmite tokens de texto directamente

Para las interfaces conversacionales en las que no es necesario mostrar los detalles de la ejecución de la herramienta por separado en la IU, usa la función de extensión textTokens() en la interfaz 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)
    }

Ejecución de un solo turno

Para tareas únicas en las que el historial de diálogo no se debe retener en la RAM (por ejemplo, etiquetado de fotos o resumen de texto efímero), llama al método runner.runSingleTurnAsync(content) directamente:

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

Cuando se invoca el método runner.runSingleTurnAsync(content) directamente, la instancia de AgentRunner asigna un ID de sesión efímero de forma interna y borra automáticamente el estado de la conversación de la memoria cuando se completa, lo que evita fugas de memoria.

Ciclo de vida de la sesión y administración de la memoria

El historial de diálogos de varios turnos se almacena en la memoria y se indexa por su valor sessionId:

  • Cambios de configuración: Conservar la instancia de AgentSession dentro de una clase ViewModel de Android preserva el historial de conversaciones en las rotaciones de pantalla de forma automática sin necesidad de canalizar cadenas en cada turno.
  • Expulsión de sesiones: Llama al método session.delete() para expulsar de forma síncrona una sesión cuando se descarta un hilo de conversación, o llama al método session.close() que no se suspende en el método ViewModel.onCleared() para liberar RAM.
  • Expulsión masiva: Llama al método runner.deleteSession(sessionId) para liberar una sesión específica o al método runner.clearAllSessions() para expulsar todas las conversaciones almacenadas en caché.
  • Ciclo de vida del ejecutor: La clase AgentRunner implementa la interfaz AutoCloseable. Cuando compartas un ejecutor entre pantallas, cierra los controladores AgentSession individuales en el método ViewModel.onCleared() de cada pantalla y llama al método runner.close() cuando cierres el ejecutor por completo.
  • Sesiones simultáneas: Una sola instancia de AgentRunner puede alojar varias sesiones distintas de forma simultánea. Cada valor de sessionId mantiene un historial de conversación aislado.

Manejo de errores con la clase GenAiException

Los errores de tiempo de ejecución durante la ejecución del agente, como cuando el agente alcanza el máximo de turnos de herramientas sin generar una respuesta o cuando la IA integrada en el dispositivo no está disponible temporalmente, se emiten o se arrojan como una excepción GenAiException (com.google.mlkit.genai.common.GenAiException). Controla los errores de forma reactiva con el operador catch de flujo de corrutinas de 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
    }

Códigos de error clave

  • GenAiException.ErrorCode.MAX_TURNS_EXCEEDED (-112): El agente superó el límite máximo de turnos (el límite de maxTurns) sin generar una respuesta final.
  • GenAiException.ErrorCode.NOT_AVAILABLE (8): El modelo de Gemini Nano o el servicio del sistema de AICore no están disponibles o listos.
  • GenAiException.ErrorCode.BUSY (9): El sistema está ocupado o la app que realiza la llamada superó su cuota.