Lors d'un workflow agentique, la classe AgentRunner et l'interface AgentSession émettent des flux d'événements réactifs représentant le processus de raisonnement du modèle, les appels d'outils et les réponses textuelles. Cette page explique comment collecter des événements et les connecter à votre UI à l'aide des coroutines Kotlin et de Jetpack Compose.
Comprendre l'interface AgentEvent
Les méthodes AgentSession.runAsync() et AgentRunner.runSingleTurnAsync() renvoient un flux Flow<AgentEvent>. Au lieu d'attendre que l'intégralité de la boucle de raisonnement se termine, votre application reçoit des mises à jour en temps réel pendant l'exécution de l'agent :
Type d'événement |
Description |
Action d'UI typique |
|
Émet des fragments de texte incrémentiels ( |
Ajoutez |
|
Émis lorsque Gemini Nano sélectionne un outil et se prépare à l'exécuter (fournit |
Affichez un indicateur de progression (par exemple, "Vérification de l'état de la commande…"). |
|
Émis lorsque l'application a terminé d'exécuter la fonction de l'outil (fournit |
Faites passer l'état de l'outil à "Terminé". |
Collecter des événements dans une classe ViewModel
Utilisez une classe Android ViewModel pour coordonner l'envoi des requêtes et exposer l'état de l'UI à vos écrans Compose :
package com.example.ai.agent.ui
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 = "default_chat_session"
) : ViewModel() {
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()
// Release session memory and cancel active coroutines
session.close()
}
}
Extraire directement des jetons de texte
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(userText).build()
session.runAsync(content)
.textTokens()
.collect { token ->
appendTokenToChatBubble(token)
}
Afficher dans Jetpack Compose
Utilisez l'état AgentUiState dans une fonction composable pour afficher le texte diffusé et l'activité de l'outil en direct :
package com.example.ai.agent.ui
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun AgentChatScreen(
viewModel: ChatViewModel,
modifier: Modifier = Modifier
) {
val state by viewModel.uiState.collectAsState()
var inputText by remember { mutableStateOf("") }
Column(modifier = modifier.fillMaxSize().padding(16.dp)) {
LazyColumn(
modifier = Modifier.weight(1f).fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(state.messages) { message ->
Card(modifier = Modifier.fillMaxWidth()) {
Text(text = message, modifier = Modifier.padding(12.dp))
}
}
if (state.activeToolStatus != null) {
item {
Text(
text = state.activeToolStatus ?: "",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.secondary
)
}
}
if (state.currentResponse.isNotEmpty()) {
item {
Card(
colors = CardDefaults.cardColors(
containerColor =
MaterialTheme.colorScheme.surfaceVariant
),
modifier = Modifier.fillMaxWidth()
) {
Text(
text = state.currentResponse,
modifier = Modifier.padding(12.dp)
)
}
}
}
}
Row(
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
TextField(
value = inputText,
onValueChange = { inputText = it },
modifier = Modifier.weight(1f),
placeholder = { Text("Ask about an order...") }
)
Button(
onClick = {
if (inputText.isNotBlank()) {
viewModel.sendMessage(inputText)
inputText = ""
}
},
enabled = !state.isLoading
) {
Text("Send")
}
}
}
}
Étapes suivantes
- Pour savoir comment définir des outils, consultez Définir des outils.
- Gérez la mémoire de session et les rotations dans Gérer les sessions et le cycle de vie.