Sự kiện phát trực tiếp và tích hợp giao diện người dùng

Trong quy trình làm việc của tác nhân, lớp AgentRunner và giao diện AgentSession sẽ phát ra các luồng sự kiện phản ứng thể hiện quy trình suy luận, lệnh gọi công cụ và phản hồi bằng văn bản của mô hình. Trang này minh hoạ cách thu thập sự kiện và kết nối sự kiện với giao diện người dùng bằng Kotlin Coroutines và Jetpack Compose.

Tìm hiểu giao diện AgentEvent

Phương thức AgentSession.runAsync() và AgentRunner.runSingleTurnAsync() trả về một luồng Flow<AgentEvent>. Thay vì đợi toàn bộ vòng lặp suy luận kết thúc, ứng dụng của bạn sẽ nhận được thông tin cập nhật theo thời gian thực khi tác nhân thực thi:

Loại sự kiện

Nội dung mô tả

Thao tác thông thường trên giao diện người dùng

AgentEvent.TextMessage

Phát ra các đoạn văn bản gia tăng (isPartial = true) theo sau là câu trả lời hoàn chỉnh cuối cùng (isPartial = false) do Gemini Nano tạo.

Thêm event.text vào bong bóng trò chuyện đang hoạt động khi event.isPartial là true (hoặc sử dụng hàm mở rộng textTokens()).

AgentEvent.FunctionCall

Được phát ra khi Gemini Nano chọn một công cụ và chuẩn bị thực thi công cụ đó (cung cấp event.name và event.args).

Hiển thị chỉ báo đang thực hiện (ví dụ: "Đang kiểm tra trạng thái đơn đặt hàng...").

AgentEvent.FunctionResponse

Được phát ra khi ứng dụng hoàn tất việc thực thi hàm công cụ (cung cấp event.name và event.response).

Chuyển trạng thái của công cụ sang đã hoàn tất.

Thu thập sự kiện trong một lớp ViewModel

Sử dụng lớp ViewModel của Android để điều phối việc gửi câu lệnh và hiển thị trạng thái giao diện người dùng cho màn hình 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()
    }
}

Trích xuất trực tiếp mã thông báo văn bản

Đối với giao diện trò chuyện mà bạn không cần hiển thị riêng thông tin chi tiết về việc thực thi công cụ trong giao diện người dùng, hãy sử dụng hàm mở rộng textTokens() trên giao diện 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)
    }

Hiển thị trong Jetpack Compose

Sử dụng trạng thái AgentUiState trong một hàm composable để hiển thị văn bản được truyền trực tuyến và hoạt động của công cụ trực tiếp:

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

Các bước tiếp theo