Stream-Ereignisse und UI-Integration

Während eines agentischen Workflows geben die Klasse AgentRunner und die Schnittstelle AgentSession reaktive Ereignisstreams aus, die den Denkprozess des Modells, Toolaufrufe und Textantworten darstellen. Auf dieser Seite wird gezeigt, wie Sie Ereignisse erfassen und mithilfe von Kotlin-Koroutinen und Jetpack Compose mit Ihrer Benutzeroberfläche verbinden.

AgentEvent-Oberfläche kennenlernen

Die Methoden AgentSession.runAsync() und AgentRunner.runSingleTurnAsync() geben einen Flow<AgentEvent>-Stream zurück. Anstatt darauf zu warten, dass der gesamte Reasoning Loop abgeschlossen ist, erhält Ihre Anwendung Echtzeit-Updates, während der Agent ausgeführt wird:

Ereignistyp

Beschreibung

Typische UI-Aktion

AgentEvent.TextMessage

Gibt inkrementelle Textblöcke (isPartial = true) aus, gefolgt von der endgültigen vollständigen Antwort (isPartial = false), die von Gemini Nano generiert wurde.

Hängt event.text an die aktive Chat-Bubble an, wenn event.isPartial den Wert true hat (oder verwendet die Erweiterungsfunktion textTokens()).

AgentEvent.FunctionCall

Wird ausgegeben, wenn Gemini Nano ein Tool auswählt und sich auf die Ausführung vorbereitet (stellt event.name und event.args bereit).

Zeigen Sie eine Fortschrittsanzeige an, z. B. „Bestellstatus wird geprüft…“.

AgentEvent.FunctionResponse

Wird ausgegeben, wenn die App die Ausführung der Toolfunktion abgeschlossen hat (stellt event.name und event.response bereit).

Ändern Sie den Toolstatus in „Abgeschlossen“.

Ereignisse in einer ViewModel-Klasse erfassen

Verwenden Sie eine Android-ViewModel-Klasse, um die Prompt-Verteilung zu koordinieren und den UI-Status für Ihre Compose-Bildschirme bereitzustellen:

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

Texttokens direkt extrahieren

Verwenden Sie für konversationelle Oberflächen, bei denen die Details zur Ausführung des Tools nicht separat in der Benutzeroberfläche angezeigt werden müssen, die Erweiterungsfunktion textTokens() auf der Schnittstelle 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)
    }

In Jetpack Compose rendern

Verwenden Sie den AgentUiState-Status in einer zusammensetzbaren Funktion, um gestreamten Text und Live-Tool-Aktivitäten zu rendern:

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

Nächste Schritte