אירועי סטרימינג ושילוב ממשק משתמש

במהלך Agentic Workflows, המחלקה AgentRunner והממשק AgentSession פולטים זרמי אירועים תגובתיים שמייצגים את תהליך החשיבה של המודל, את הפעלות הכלים ואת התשובות בטקסט. בדף הזה מוסבר איך לאסוף אירועים ולקשר אותם לממשק המשתמש באמצעות Kotlin Coroutines ו-Jetpack Compose.

הסבר על הממשק של AgentEvent

השיטות AgentSession.runAsync() ו-AgentRunner.runSingleTurnAsync() מחזירות זרם Flow<AgentEvent>. במקום לחכות עד שכל לולאת החשיבה הרציונלית תסתיים, האפליקציה מקבלת עדכונים בזמן אמת בזמן שהסוכן פועל:

סוג האירוע

תיאור

פעולה אופיינית בממשק המשתמש

AgentEvent.TextMessage

פולט קטעי טקסט מצטברים (isPartial = true) ואחריהם את התשובה המלאה הסופית (isPartial = false) שנוצרה על ידי Gemini Nano.

מוסיפים את event.text לבועת הצ'אט הפעילה כש-event.isPartial הוא true (או משתמשים בפונקציית התוסף textTokens()).

AgentEvent.FunctionCall

מופעל כש-Gemini Nano בוחר כלי ומתכונן להפעיל אותו (מספק את event.name ו-event.args).

הצגת אינדיקטור של תהליך שמתבצע (לדוגמה, "בודק את סטטוס ההזמנה…").

AgentEvent.FunctionResponse

נשלח כשהאפליקציה מסיימת להריץ את פונקציית הכלי (מספק את event.name ו-event.response).

מעבירים את סטטוס הכלי למצב "הושלם".

איסוף אירועים בכיתה ViewModel

משתמשים במחלקה ViewModel של Android כדי לתאם את שליחת ההנחיות ולחשוף את מצב ממשק המשתמש למסכי 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()
    }
}

חילוץ טוקנים של טקסט ישירות

בממשקי שיחה שבהם לא צריך להציג בנפרד בממשק המשתמש את פרטי ההפעלה של הכלי, משתמשים בפונקציית ההרחבה textTokens() בממשק 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)
    }

עיבוד ב-Jetpack Compose

משתמשים במצב AgentUiState בפונקציה קומפוזבילית כדי לעבד טקסט בסטרימינג ופעילות של כלי בזמן אמת:

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

השלבים הבאים