diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/MainActivity.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/MainActivity.kt index 8cbafed1f..ec379528f 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/MainActivity.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/MainActivity.kt @@ -28,6 +28,7 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.google.firebase.quickstart.ai.feature.live.BidiViewModel import com.google.firebase.quickstart.ai.feature.hybrid.HybridInferenceViewModel +import com.google.firebase.quickstart.ai.feature.hybrid.SystemInstructionViewModel import com.google.firebase.quickstart.ai.feature.text.ChatViewModel import com.google.firebase.quickstart.ai.feature.text.ServerPromptTemplateViewModel import com.google.firebase.quickstart.ai.feature.text.SvgViewModel @@ -37,6 +38,7 @@ import com.google.firebase.quickstart.ai.ui.StreamRealtimeScreen import com.google.firebase.quickstart.ai.ui.StreamRealtimeVideoScreen import com.google.firebase.quickstart.ai.ui.HybridInferenceScreen import com.google.firebase.quickstart.ai.ui.SvgScreen +import com.google.firebase.quickstart.ai.ui.SystemInstructionScreen import com.google.firebase.quickstart.ai.ui.navigation.FIREBASE_AI_SAMPLES import com.google.firebase.quickstart.ai.ui.navigation.MainMenuScreen import com.google.firebase.quickstart.ai.ui.navigation.ScreenType @@ -123,6 +125,10 @@ class MainActivity : ComponentActivity() { ScreenType.HYBRID -> { (vm as? HybridInferenceViewModel)?.let { HybridInferenceScreen(it) } } + + ScreenType.SYSTEM_INSTRUCTION -> { + (vm as? SystemInstructionViewModel)?.let { SystemInstructionScreen(it) } + } } } } diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/hybrid/SystemInstructionViewModel.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/hybrid/SystemInstructionViewModel.kt new file mode 100644 index 000000000..bec9fea43 --- /dev/null +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/hybrid/SystemInstructionViewModel.kt @@ -0,0 +1,267 @@ +package com.google.firebase.quickstart.ai.feature.hybrid + +import android.graphics.Bitmap +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.google.firebase.Firebase +import com.google.firebase.ai.DownloadStatus +import com.google.firebase.ai.InferenceMode +import com.google.firebase.ai.InferenceSource +import com.google.firebase.ai.OnDeviceConfig +import com.google.firebase.ai.OnDeviceModelStatus +import com.google.firebase.ai.ai +import com.google.firebase.ai.type.Content +import com.google.firebase.ai.type.GenerativeBackend +import com.google.firebase.ai.type.PublicPreviewAPI +import com.google.firebase.ai.type.content +import com.google.firebase.quickstart.ai.ui.SystemInstructionUiState +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable + +@Serializable +object SystemInstructionRoute + +enum class SystemInstructionCase(val label: String) { + SINGLE_TEXT("Single TextPart"), + MULTIPLE_TEXT("Multiple TextParts"), + NON_TEXT_PART("Non-Text Part (Image)"), + NONE("No System Instruction") +} + +enum class GenerationMethod(val label: String) { + UNARY("generateContent"), + STREAMING("generateContentStream") +} + +@OptIn(PublicPreviewAPI::class) +enum class InferenceModeOption(val label: String, val mode: InferenceMode) { + ONLY_ON_DEVICE("ONLY_ON_DEVICE", InferenceMode.ONLY_ON_DEVICE), + PREFER_ON_DEVICE("PREFER_ON_DEVICE", InferenceMode.PREFER_ON_DEVICE), + PREFER_IN_CLOUD("PREFER_IN_CLOUD", InferenceMode.PREFER_IN_CLOUD), + ONLY_IN_CLOUD("ONLY_IN_CLOUD", InferenceMode.ONLY_IN_CLOUD) +} + +@OptIn(PublicPreviewAPI::class) +class SystemInstructionViewModel : ViewModel() { + private val _uiState = MutableStateFlow(SystemInstructionUiState()) + val uiState: StateFlow = _uiState + + private var testJob: Job? = null + + private val statusCheckModel = Firebase.ai(backend = GenerativeBackend.googleAI()).generativeModel( + modelName = "gemini-3.5-flash-lite", + onDeviceConfig = OnDeviceConfig(mode = InferenceMode.PREFER_ON_DEVICE) + ) + + init { + checkAndDownloadModel() + } + + private fun checkAndDownloadModel() { + viewModelScope.launch { + try { + val status = statusCheckModel.onDeviceExtension?.checkStatus() + val statusText = when (status) { + OnDeviceModelStatus.AVAILABLE -> "Model available" + OnDeviceModelStatus.DOWNLOADABLE -> "Model downloadable" + OnDeviceModelStatus.DOWNLOADING -> "Model downloading..." + else -> "On-device model unavailable" + } + _uiState.update { it.copy(modelStatus = statusText) } + + if (status == OnDeviceModelStatus.DOWNLOADABLE) { + statusCheckModel.onDeviceExtension?.download()?.collect { downloadStatus -> + when (downloadStatus) { + is DownloadStatus.DownloadStarted -> { + _uiState.update { it.copy(modelStatus = "Downloading model...") } + } + is DownloadStatus.DownloadInProgress -> { + val progress = downloadStatus.totalBytesDownloaded + _uiState.update { it.copy(modelStatus = "Downloading: $progress bytes") } + } + is DownloadStatus.DownloadCompleted -> { + _uiState.update { it.copy(modelStatus = "Model available") } + } + is DownloadStatus.DownloadFailed -> { + _uiState.update { + it.copy( + modelStatus = "Download failed", + errorMessage = "Model download failed" + ) + } + } + } + } + } + } catch (e: Exception) { + _uiState.update { + it.copy( + modelStatus = "Error checking status", + errorMessage = e.localizedMessage ?: e.toString() + ) + } + } + } + } + + private fun buildSystemInstruction( + case: SystemInstructionCase, + primaryInstruction: String, + secondaryInstruction: String, + testImage: Bitmap? + ): Content? { + return when (case) { + SystemInstructionCase.SINGLE_TEXT -> content { + text(primaryInstruction) + } + SystemInstructionCase.MULTIPLE_TEXT -> content { + text(primaryInstruction) + text(secondaryInstruction) + } + SystemInstructionCase.NON_TEXT_PART -> content { + text(primaryInstruction) + if (testImage != null) { + image(testImage) + } + } + SystemInstructionCase.NONE -> null + } + } + + fun runTest( + promptText: String, + primaryInstruction: String, + secondaryInstruction: String, + case: SystemInstructionCase, + mode: InferenceMode, + method: GenerationMethod, + testImage: Bitmap? = null + ) { + testJob?.cancel() + testJob = viewModelScope.launch { + _uiState.update { + it.copy( + isLoading = true, + errorMessage = null, + responseText = null, + inferenceSource = null, + logOutput = "Running inference..." + ) + } + + val systemInstruction = + buildSystemInstruction(case, primaryInstruction, secondaryInstruction, testImage) + val partsSummary = systemInstruction?.parts?.joinToString(", ") { part -> + part::class.java.simpleName + } ?: "null" + + Log.i(TAG, "==========================================================") + Log.i(TAG, ">>> [SYSTEM INSTRUCTION TEST - INPUT] >>>") + Log.i(TAG, "Case: ${case.name} ($partsSummary)") + Log.i(TAG, "Inference Mode: $mode") + Log.i(TAG, "Generation Method: ${method.label}") + Log.i(TAG, "Prompt: \"$promptText\"") + Log.i(TAG, "==========================================================") + + try { + val model = Firebase.ai(backend = GenerativeBackend.googleAI()).generativeModel( + modelName = "gemini-3.5-flash-lite", + onDeviceConfig = OnDeviceConfig(mode = mode), + systemInstruction = systemInstruction + ) + + var fullText = "" + var source: InferenceSource? = null + + when (method) { + GenerationMethod.UNARY -> { + val response = model.generateContent(promptText) + fullText = response.text.orEmpty() + source = response.inferenceSource + } + GenerationMethod.STREAMING -> { + model.generateContentStream(promptText).collect { chunk -> + fullText += chunk.text.orEmpty() + source = chunk.inferenceSource + _uiState.update { + it.copy( + responseText = fullText, + inferenceSource = formatInferenceSource(source) + ) + } + } + } + } + + val sourceName = formatInferenceSource(source) + Log.i(TAG, "<<< [SYSTEM INSTRUCTION TEST - SUCCESS] <<<") + Log.i(TAG, "Inference Source: $sourceName") + Log.i(TAG, "Response Text: \"$fullText\"") + + val summaryLog = """ + === INPUT === + Case: ${case.label} (parts: $partsSummary) + Mode: $mode + Method: ${method.label} + Prompt: "$promptText" + + === OUTPUT (SUCCESS) === + Inference Source: $sourceName + Response: + $fullText + """.trimIndent() + + _uiState.update { + it.copy( + isLoading = false, + responseText = fullText, + inferenceSource = sourceName, + logOutput = summaryLog + ) + } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + val causeName = e.cause?.let { " (cause: ${it::class.java.simpleName})" }.orEmpty() + Log.e(TAG, "<<< [SYSTEM INSTRUCTION TEST - FAILURE] <<<", e) + + val errorLog = """ + === INPUT === + Case: ${case.label} (parts: $partsSummary) + Mode: $mode + Method: ${method.label} + + === OUTPUT (FAILURE) === + Exception: ${e::class.java.simpleName}$causeName + Message: ${e.localizedMessage ?: e.toString()} + """.trimIndent() + + _uiState.update { + it.copy( + isLoading = false, + errorMessage = "${e::class.java.simpleName}$causeName: ${e.localizedMessage ?: e.toString()}", + logOutput = errorLog + ) + } + } + } + } + + private fun formatInferenceSource(source: InferenceSource?): String = + when (source) { + InferenceSource.ON_DEVICE -> "ON_DEVICE" + InferenceSource.IN_CLOUD -> "IN_CLOUD" + null -> "UNKNOWN" + else -> source.toString() + } + + companion object { + private const val TAG = "SystemInstructionDemo" + } +} diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/SystemInstructionScreen.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/SystemInstructionScreen.kt new file mode 100644 index 000000000..5e1685ccd --- /dev/null +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/SystemInstructionScreen.kt @@ -0,0 +1,287 @@ +package com.google.firebase.quickstart.ai.ui + +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.FilterChip +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.google.firebase.ai.type.PublicPreviewAPI +import com.google.firebase.quickstart.ai.MainActivity +import com.google.firebase.quickstart.ai.feature.hybrid.GenerationMethod +import com.google.firebase.quickstart.ai.feature.hybrid.InferenceModeOption +import com.google.firebase.quickstart.ai.feature.hybrid.SystemInstructionCase +import com.google.firebase.quickstart.ai.feature.hybrid.SystemInstructionViewModel + +@OptIn(PublicPreviewAPI::class) +@Composable +fun SystemInstructionScreen( + viewModel: SystemInstructionViewModel = viewModel() +) { + val uiState by viewModel.uiState.collectAsState() + var prompt by rememberSaveable { mutableStateOf("Tell me a fun fact about space.") } + var primaryInstruction by rememberSaveable { + mutableStateOf("Respond like a 17th-century pirate in 1-2 short sentences.") + } + var secondaryInstruction by rememberSaveable { + mutableStateOf("Always end your response with the phrase 'Arrr, matey!'") + } + var selectedCase by rememberSaveable { mutableStateOf(SystemInstructionCase.SINGLE_TEXT) } + var selectedMode by rememberSaveable { mutableStateOf(InferenceModeOption.ONLY_ON_DEVICE) } + var selectedMethod by rememberSaveable { mutableStateOf(GenerationMethod.UNARY) } + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer + ) + ) { + Column(modifier = Modifier.padding(12.dp)) { + Text( + text = "On-Device Model Status", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSecondaryContainer + ) + Text( + text = uiState.modelStatus, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSecondaryContainer + ) + } + } + + Text( + text = "1. System Instruction Case:", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + Row( + modifier = Modifier.horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + SystemInstructionCase.entries.forEach { case -> + FilterChip( + selected = selectedCase == case, + onClick = { selectedCase = case }, + label = { Text(case.label) } + ) + } + } + + if (selectedCase != SystemInstructionCase.NONE) { + OutlinedTextField( + value = primaryInstruction, + onValueChange = { primaryInstruction = it }, + label = { Text("System Instruction (TextPart 1)") }, + modifier = Modifier.fillMaxWidth(), + maxLines = 3 + ) + } + + if (selectedCase == SystemInstructionCase.MULTIPLE_TEXT) { + OutlinedTextField( + value = secondaryInstruction, + onValueChange = { secondaryInstruction = it }, + label = { Text("System Instruction (TextPart 2 - Concatenated)") }, + modifier = Modifier.fillMaxWidth(), + maxLines = 3 + ) + } + + OutlinedTextField( + value = prompt, + onValueChange = { prompt = it }, + label = { Text("User Prompt") }, + modifier = Modifier.fillMaxWidth(), + maxLines = 3 + ) + + Text( + text = "2. Inference Mode:", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + Row( + modifier = Modifier.horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + InferenceModeOption.entries.forEach { modeOption -> + FilterChip( + selected = selectedMode == modeOption, + onClick = { selectedMode = modeOption }, + label = { Text(modeOption.label) } + ) + } + } + + Text( + text = "3. Generation Method:", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + Row( + modifier = Modifier.horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + GenerationMethod.entries.forEach { method -> + FilterChip( + selected = selectedMethod == method, + onClick = { selectedMethod = method }, + label = { Text(method.label) } + ) + } + } + + Button( + onClick = { + viewModel.runTest( + promptText = prompt, + primaryInstruction = primaryInstruction, + secondaryInstruction = secondaryInstruction, + case = selectedCase, + mode = selectedMode.mode, + method = selectedMethod, + testImage = MainActivity.catImage + ) + }, + enabled = !uiState.isLoading, + modifier = Modifier.fillMaxWidth() + ) { + if (uiState.isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + color = MaterialTheme.colorScheme.onPrimary, + strokeWidth = 2.dp + ) + Spacer(modifier = Modifier.size(8.dp)) + } + Text("Run System Instruction Test") + } + + val errorMessage = uiState.errorMessage + if (errorMessage != null) { + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.errorContainer + ), + modifier = Modifier.fillMaxWidth() + ) { + Column(modifier = Modifier.padding(12.dp)) { + Text( + text = "Error", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onErrorContainer + ) + Text( + text = errorMessage, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onErrorContainer + ) + } + } + } + + val responseText = uiState.responseText + if (responseText != null) { + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer + ), + modifier = Modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Response", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + Surface( + color = MaterialTheme.colorScheme.primary, + shape = MaterialTheme.shapes.small + ) { + Text( + text = uiState.inferenceSource ?: "UNKNOWN", + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), + color = MaterialTheme.colorScheme.onPrimary, + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.Bold + ) + } + } + Text( + text = responseText, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + } + } + + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant + ), + modifier = Modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier.padding(12.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = "Diagnostics & Logcat Summary", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold + ) + Text( + text = uiState.logOutput, + style = MaterialTheme.typography.bodySmall, + fontFamily = FontFamily.Monospace + ) + } + } + } +} diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/SystemInstructionUiState.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/SystemInstructionUiState.kt new file mode 100644 index 000000000..5724b8344 --- /dev/null +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/SystemInstructionUiState.kt @@ -0,0 +1,10 @@ +package com.google.firebase.quickstart.ai.ui + +data class SystemInstructionUiState( + val modelStatus: String = "Checking model status...", + val isLoading: Boolean = false, + val responseText: String? = null, + val inferenceSource: String? = null, + val logOutput: String = "Configure system instruction options above and tap Run.", + val errorMessage: String? = null +) diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/navigation/FirebaseAISamples.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/navigation/FirebaseAISamples.kt index 8f6f7b4aa..808c8d8a1 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/navigation/FirebaseAISamples.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/navigation/FirebaseAISamples.kt @@ -2,6 +2,8 @@ package com.google.firebase.quickstart.ai.ui.navigation import com.google.firebase.quickstart.ai.feature.hybrid.HybridInferenceRoute import com.google.firebase.quickstart.ai.feature.hybrid.HybridInferenceViewModel +import com.google.firebase.quickstart.ai.feature.hybrid.SystemInstructionRoute +import com.google.firebase.quickstart.ai.feature.hybrid.SystemInstructionViewModel import com.google.firebase.quickstart.ai.feature.live.StreamAudioViewModel import com.google.firebase.quickstart.ai.feature.live.StreamRealtimeAudioRoute import com.google.firebase.quickstart.ai.feature.live.StreamRealtimeVideoRoute @@ -197,5 +199,13 @@ val FIREBASE_AI_SAMPLES = listOf( screenType = ScreenType.HYBRID, viewModelClass = HybridInferenceViewModel::class, categories = listOf(Category.HYBRID) + ), + Sample( + title = "Hybrid System Instructions", + description = "Test systemInstruction (single text, multiple text parts, and non-text part validation) across On-Device, Hybrid, and Cloud inference modes.", + route = SystemInstructionRoute, + screenType = ScreenType.SYSTEM_INSTRUCTION, + viewModelClass = SystemInstructionViewModel::class, + categories = listOf(Category.HYBRID) ) ) diff --git a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/navigation/Sample.kt b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/navigation/Sample.kt index d800f0184..348c7d32a 100644 --- a/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/navigation/Sample.kt +++ b/firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/navigation/Sample.kt @@ -21,7 +21,8 @@ enum class ScreenType { SERVER_PROMPT, BIDI, BIDI_VIDEO, - HYBRID + HYBRID, + SYSTEM_INSTRUCTION } data class Sample( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ee0f5d764..bb5552496 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -51,8 +51,8 @@ coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil3C coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil3Compose" } coil-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil3Compose" } compose-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "composeNavigation"} -firebase-ai = { module = "com.google.firebase:firebase-ai" } -firebase-ai-ondevice = { module = "com.google.firebase:firebase-ai-ondevice", version = "16.0.0-beta05" } +firebase-ai = { module = "com.google.firebase:firebase-ai", version = "18.0.0" } +firebase-ai-ondevice = { module = "com.google.firebase:firebase-ai-ondevice", version = "16.0.0-beta06" } firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" } junit = { group = "junit", name = "junit", version.ref = "junit" } kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerializationCore" }