Criar fluxos de trabalho com agentes usando o Gemini Nano

Com os Agentic Workflows no dispositivo do Kit de ML (com.google.mlkit:genai-agent, disponíveis no programa de prévia para desenvolvedores do AICore), é possível criar agentes de conversação autônomos e multiturno com tecnologia do Gemini Nano e do serviço do sistema Android AICore.

Criada com base na classe GenerativeModel, que fornece acesso direto ao modelo, a classe AgentRunner automatiza loops do agente completos: planejamento de ações em várias etapas, invocação de ferramentas tipadas (como consultar um repositório de apps, chamar uma API de back-end ou ler um banco de dados local), avaliação de resultados de ferramentas e gerenciamento de contexto de conversação localmente no dispositivo com latência de rede zero e privacidade do usuário total. Enquanto a classe GenerativeModel oferece suporte ao histórico de vários turnos gerenciado pelo cliente usando um objeto List<Content> (com papéis USER e MODEL) e um valor sessionId, a classe AgentRunner oferece sessões de vários turnos gerenciadas (usando a interface AgentSession) que registram o histórico de conversas na memória automaticamente em vários turnos para que você não precise gerenciar as transcrições manualmente. As declarações de ferramentas estáticas e as instruções do sistema também são armazenadas em cache automaticamente no dispositivo, reduzindo a latência do tempo até o primeiro token sem configuração extra.

Principais componentes

  • A classe Agent: declara instruções, ferramentas disponíveis e proteções de execução usando o método Agent.builder(). O padrão é maxTurns = 10 automaticamente.
  • A classe AgentRunner: criada com base na classe GenerativeModel, ela organiza loops de raciocínio autônomos e sessões gerenciadas com modelos no dispositivo. Seguro para a linha de execução principal por padrão em Dispatchers.IO, com temperature definido como null (que segue o padrão do modelo; transmita temperature = 0.0f para decodificação gananciosa).
  • A interface AgentSession: identificador de sessão de conversa de várias interações com escopo definido, obtido usando o método runner.getOrCreateSession(sessionId), que registra automaticamente o histórico de conversas na memória em várias interações, com os métodos runAsync(content) e close().
  • A interface AgentEvent: o stream reativo Flow do Kotlin que emite tokens de texto e atualizações de ferramentas.
  • A classe AgentTool: funções tipadas que podem ser chamadas pelo modelo, definidas usando as AppFunctions do Android (@AppFunctionDeclaration).
  • A classe GenAiException: hierarquia de exceções tipadas (com.google.mlkit.genai.common.GenAiException) que sinaliza estados de falha, como MAX_TURNS_EXCEEDED, NOT_AVAILABLE ou BUSY.

Pré-requisitos e dependências

Verifique se o ambiente de desenvolvimento e a configuração do Gradle atendem aos seguintes requisitos antes de integrar a API Agent.

Requisitos de plataforma e hardware

Configuração do Gradle

Adicione a dependência genai-agent ao arquivo build.gradle.kts do 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")
}

Embora o genai-agent em si não exija um processador de anotações ou um plug-in de compilador, a definição de ferramentas com androidx.appfunctions exige o plug-in KSP appfunctions-compiler.

Guia de início rápido: inicializar um agente

O exemplo de código a seguir mostra como instanciar um agente e um executor no 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)

Definir ferramentas

O modelo pode chamar ferramentas tipadas para consultar dados do app, se comunicar com serviços de back-end ou acionar ações no app. As ferramentas são definidas de forma declarativa usando as AppFunctions do 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",
        )
    }
}

Registre o serviço gerado no arquivo AndroidManifest.xml do 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>

Restringir o acesso do autor da chamada com a constante ACCESS_LEVEL_SELF

As AppFunctions não precisam ser públicas para o sistema ou para autores de chamada externos. No Android, o controle de acesso é feito por níveis de acesso do autor da chamada, em vez de inspecionar as identidades dele.

Por padrão (usando a constante ACCESS_LEVEL_ANDROID_TRUSTED), as AppFunctions podem ser chamadas por assistentes no dispositivo certificados pela plataforma que têm a permissão do sistema EXECUTE_APP_FUNCTIONS. Se a ferramenta for destinada estritamente ao agente interno do app (por exemplo, gerenciar carrinhos de usuários ou registros de banco de dados particulares), restrinja o acesso do autor da chamada usando a 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."
    }
}

Quando restrita com a constante ACCESS_LEVEL_SELF, a plataforma impede que superfícies externas e assistentes do sistema descubram ou executem a função por IPC.

Configurar o agente

Configure uma instância Agent com instruções do sistema e ative as AppFunctions. Em seguida, inicialize a instância 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)

Se quiser, chame o método addAppFunctionIds("fetch_order_details", "cancel_order") para limitar a descoberta estritamente a IDs de AppFunction específicos, em vez de expor todas as funções no app.

Padrões do agente

  • Limites de turnos: a classe Agent.Builder usa maxTurns = 10 como padrão automaticamente como uma condição de encerramento para evitar loops infinitos de chamadas de ferramentas. Não é necessário chamar o método setMaxTurns(10) manualmente para fluxos de trabalho padrão de conversação ou chamada de ferramenta.
  • Decodificação gananciosa determinista: a classe AgentRunner define o parâmetro padrão temperature como null, de acordo com o padrão do modelo subjacente. Para seleção de ferramentas determinística e conformidade com o esquema JSON no dispositivo, transmita temperature = 0.0f (temperatura) para o construtor AgentRunner. Evite definir temperaturas no estilo de nuvem (> 0,4f), que aumentam os riscos de alucinação e a degradação do esquema em modelos para dispositivos móveis.

Execução em vários turnos e eventos de streaming

Consiga uma instância de AgentSession usando o método runner.getOrCreateSession(sessionId) em uma classe ViewModel do Android. A sessão retorna um stream Flow<AgentEvent> que emite tokens de texto em tempo real e atualizações de execução de ferramentas:

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

Fazer streaming de tokens de texto diretamente

Para interfaces de conversação em que os detalhes de execução da ferramenta não precisam ser mostrados separadamente na interface, use a função de extensão textTokens() na 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)
    }

Execução de interação única

Para tarefas únicas em que o histórico de diálogo não deve ser retido na RAM (por exemplo, marcação de fotos ou resumo de texto temporário), chame o método runner.runSingleTurnAsync(content) diretamente:

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

Ao invocar o método runner.runSingleTurnAsync(content) diretamente, a instância AgentRunner aloca um ID de sessão temporário internamente e exclui automaticamente o estado da conversa da memória após a conclusão, evitando vazamentos de memória.

Ciclo de vida da sessão e gerenciamento de memória

O histórico de diálogos de várias interações é armazenado na memória e indexado pelo valor sessionId:

  • Mudanças de configuração: manter a instância AgentSession dentro de uma classe ViewModel do Android preserva o histórico de conversas em rotações de tela automaticamente, sem precisar de strings de encadeamento em cada turno.
  • Remoção de sessão: chame o método session.delete() para remover uma sessão de forma síncrona quando uma conversa for dispensada ou chame o método session.close() não suspenso no método ViewModel.onCleared() para liberar RAM.
  • Remoção em massa: chame o método runner.deleteSession(sessionId) para liberar uma sessão específica ou o método runner.clearAllSessions() para remover todas as conversas em cache.
  • Ciclo de vida do executor: a classe AgentRunner implementa a interface AutoCloseable. Ao compartilhar um executor entre telas, feche os identificadores AgentSession individuais no método ViewModel.onCleared() de cada tela e chame o método runner.close() ao encerrar o executor completamente.
  • Sessões simultâneas: uma única instância do AgentRunner pode hospedar várias sessões distintas simultaneamente. Cada valor sessionId mantém um histórico de conversas isolado.

Tratamento de erros com a classe GenAiException

As falhas de execução durante a execução do agente, como quando o agente atinge o máximo de turnos de ferramenta sem gerar uma resposta ou quando a IA no dispositivo está temporariamente indisponível, são emitidas ou geradas como uma exceção GenAiException (com.google.mlkit.genai.common.GenAiException). Trate as falhas de forma reativa usando o operador catch do fluxo de corrotinas do 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 erro principais

  • GenAiException.ErrorCode.MAX_TURNS_EXCEEDED (-112): o agente excedeu o limite máximo de turnos (o limite maxTurns) sem gerar uma resposta final.
  • GenAiException.ErrorCode.NOT_AVAILABLE (8): o modelo Gemini Nano ou o serviço do sistema AICore não está disponível ou pronto.
  • GenAiException.ErrorCode.BUSY (9): o sistema está ocupado ou o app de ligação excedeu a cota.