From c465e1da5b50da58919e4798da8f3bf4e4338d87 Mon Sep 17 00:00:00 2001 From: Damian Momot Date: Wed, 22 Jul 2026 01:28:48 -0700 Subject: [PATCH] refactor(mlkit): extract ML Kit support into a dedicated mlkit module Move the ML Kit GenAI prompt code, conversions, tracing, and tests out of the core module into a standalone mlkit module and wire it up in the Gradle and Blaze build. Change is backward compatible on code-level but it does require new artifact to be referenced during build time PiperOrigin-RevId: 951956680 --- core/build.gradle.kts | 1 - .../RoomSessionServiceInstrumentedTest.kt | 74 +--------- examples/android/build.gradle.kts | 1 + .../android/mlkitchat/MlKitChatActivity.kt | 4 +- .../roomsession/RoomSessionActivity.kt | 4 +- .../WizardApprenticeAgent.kt | 4 +- mlkit/build.gradle.kts | 121 ++++++++++++++++ .../kt}/mlkit/GenaiPromptInstrumentedTest.kt | 3 +- .../RoomSessionGenaiPromptInstrumentedTest.kt | 132 ++++++++++++++++++ mlkit/src/main/AndroidManifest.xml | 10 ++ .../src/main}/assets/test-image.png | Bin .../com/google/adk/kt}/mlkit/GenaiPrompt.kt | 10 +- .../adk/kt}/mlkit/GenaiPromptConversions.kt | 2 +- .../adk/kt}/mlkit/GenaiPromptTracing.kt | 3 +- .../GenerateContentResponseAggregator.kt | 2 +- .../adk/kt}/mlkit/GenerativeModelHelpers.kt | 2 +- .../google/adk/kt/models/mlkit/GenaiPrompt.kt | 27 ++++ .../kt/utils/mlkit/GenerativeModelHelpers.kt | 27 ++++ .../kt}/mlkit/GenaiPromptConversionsTest.kt | 6 +- .../adk/kt}/mlkit/GenaiPromptHostTest.kt | 2 +- .../adk/kt}/mlkit/GenaiPromptTracingTest.kt | 2 +- .../GenerateContentResponseAggregatorTest.kt | 2 +- .../kt}/mlkit/GenerativeModelHelpersTest.kt | 2 +- .../src/test/resources/robolectric.properties | 3 + settings.gradle.kts | 4 + 25 files changed, 351 insertions(+), 97 deletions(-) create mode 100644 mlkit/build.gradle.kts rename {core/src/androidDeviceTest/kotlin/com/google/adk/kt/models => mlkit/src/androidTest/kotlin/com/google/adk/kt}/mlkit/GenaiPromptInstrumentedTest.kt (98%) create mode 100644 mlkit/src/androidTest/kotlin/com/google/adk/kt/mlkit/RoomSessionGenaiPromptInstrumentedTest.kt create mode 100644 mlkit/src/main/AndroidManifest.xml rename {core/src/androidHostTest => mlkit/src/main}/assets/test-image.png (100%) rename {core/src/androidMain/kotlin/com/google/adk/kt/models => mlkit/src/main/kotlin/com/google/adk/kt}/mlkit/GenaiPrompt.kt (90%) rename {core/src/androidMain/kotlin/com/google/adk/kt/utils => mlkit/src/main/kotlin/com/google/adk/kt}/mlkit/GenaiPromptConversions.kt (99%) rename {core/src/androidMain/kotlin/com/google/adk/kt/models => mlkit/src/main/kotlin/com/google/adk/kt}/mlkit/GenaiPromptTracing.kt (98%) rename {core/src/androidMain/kotlin/com/google/adk/kt/utils => mlkit/src/main/kotlin/com/google/adk/kt}/mlkit/GenerateContentResponseAggregator.kt (98%) rename {core/src/androidMain/kotlin/com/google/adk/kt/utils => mlkit/src/main/kotlin/com/google/adk/kt}/mlkit/GenerativeModelHelpers.kt (99%) create mode 100644 mlkit/src/main/kotlin/com/google/adk/kt/models/mlkit/GenaiPrompt.kt create mode 100644 mlkit/src/main/kotlin/com/google/adk/kt/utils/mlkit/GenerativeModelHelpers.kt rename {core/src/androidHostTest/kotlin/com/google/adk/kt/utils => mlkit/src/test/kotlin/com/google/adk/kt}/mlkit/GenaiPromptConversionsTest.kt (98%) rename {core/src/androidHostTest/kotlin/com/google/adk/kt/models => mlkit/src/test/kotlin/com/google/adk/kt}/mlkit/GenaiPromptHostTest.kt (99%) rename {core/src/androidHostTest/kotlin/com/google/adk/kt/models => mlkit/src/test/kotlin/com/google/adk/kt}/mlkit/GenaiPromptTracingTest.kt (99%) rename {core/src/androidHostTest/kotlin/com/google/adk/kt/utils => mlkit/src/test/kotlin/com/google/adk/kt}/mlkit/GenerateContentResponseAggregatorTest.kt (99%) rename {core/src/androidHostTest/kotlin/com/google/adk/kt/utils => mlkit/src/test/kotlin/com/google/adk/kt}/mlkit/GenerativeModelHelpersTest.kt (98%) create mode 100644 mlkit/src/test/resources/robolectric.properties diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 75d57dde..0456a30d 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -122,7 +122,6 @@ kotlin { implementation( libs.google.auth.oauth2.http ) // Android compatible version or use separate for Android if needed. - implementation(libs.google.mlkit.genai.prompt) implementation(libs.androidx.room.runtime) implementation(libs.androidx.room.ktx) } diff --git a/core/src/androidDeviceTest/kotlin/com/google/adk/kt/sessions/room/RoomSessionServiceInstrumentedTest.kt b/core/src/androidDeviceTest/kotlin/com/google/adk/kt/sessions/room/RoomSessionServiceInstrumentedTest.kt index 653b4713..64138747 100644 --- a/core/src/androidDeviceTest/kotlin/com/google/adk/kt/sessions/room/RoomSessionServiceInstrumentedTest.kt +++ b/core/src/androidDeviceTest/kotlin/com/google/adk/kt/sessions/room/RoomSessionServiceInstrumentedTest.kt @@ -20,24 +20,13 @@ import android.content.Context import androidx.compose.ui.test.junit4.v2.createComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry -import com.google.adk.kt.agents.Instruction -import com.google.adk.kt.agents.LlmAgent -import com.google.adk.kt.agents.RunConfig -import com.google.adk.kt.agents.StreamingMode import com.google.adk.kt.annotations.FrameworkInternalApi import com.google.adk.kt.events.Event -import com.google.adk.kt.models.mlkit.GenaiPrompt -import com.google.adk.kt.runners.InMemoryRunner import com.google.adk.kt.sessions.SessionKey import com.google.adk.kt.types.Content import com.google.adk.kt.types.Part import com.google.adk.kt.types.Role -import com.google.adk.kt.utils.mlkit.GenerativeModelHelpers import com.google.common.truth.Truth.assertThat -import com.google.mlkit.genai.prompt.ModelConfig -import com.google.mlkit.genai.prompt.ModelPreference -import com.google.mlkit.genai.prompt.ModelReleaseStage -import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking import org.junit.After import org.junit.Before @@ -49,10 +38,10 @@ import org.junit.runner.RunWith * On-device instrumented tests for [RoomSessionService], exercising the real Android SQLite stack * (the host-side `RoomSessionServiceTest` runs against a simulated DB). * - * [appendEvent_persistsAcrossDatabaseReopen] is a deterministic persistence check that needs no - * model. [runTurn_withGenaiPrompt_persistsEventsAcrossReopen] is an end-to-end check that drives a - * real on-device Gemini Nano turn through an [InMemoryRunner] backed by [RoomSessionService], - * mirroring the ML Kit instrumentation setup; it requires a device with AI Core / Gemini Nano. + * [appendEvent_persistsAcrossDatabaseReopen] and + * [appendEvent_partWithOpaqueData_persistsAndDropsOpaqueData] are deterministic persistence checks + * that need no model. The end-to-end check that drives a real on-device Gemini Nano turn through + * [RoomSessionService] lives in the `mlkit` module's `RoomSessionGenaiPromptInstrumentedTest`. */ @RunWith(AndroidJUnit4::class) class RoomSessionServiceInstrumentedTest { @@ -139,63 +128,8 @@ class RoomSessionServiceInstrumentedTest { } } - @Test - fun runTurn_withGenaiPrompt_persistsEventsAcrossReopen(): Unit = runBlocking { - // initGenerativeModel is suspend and downloads Gemini Nano if needed. - val generativeModel = GenerativeModelHelpers.initGenerativeModel { - modelConfig = - ModelConfig.builder() - .apply { - releaseStage = ModelReleaseStage.STABLE - preference = ModelPreference.FULL - } - .build() - } - val agent = - LlmAgent( - name = "room_session_agent", - model = GenaiPrompt.create(generativeModel, name = "gemini-nano"), - instruction = Instruction("You are a helpful assistant. Answer in one short sentence."), - ) - val runner = InMemoryRunner(agent = agent, appName = APP_NAME, sessionService = sessionService) - - val userId = "user-e2e" - val sessionId = "session-e2e" - val events = - runner - .runAsync( - userId = userId, - sessionId = sessionId, - newMessage = Content(role = Role.USER, parts = listOf(Part(text = QUESTION))), - runConfig = RunConfig(streamingMode = StreamingMode.NONE), - ) - .toList() - - assertThat(events).isNotEmpty() - assertThat(events.mapNotNull { it.errorCode }).isEmpty() - - // The turn's events were persisted to the Room-backed session. - val key = SessionKey(APP_NAME, userId, sessionId) - val session = sessionService.getSession(key) - assertThat(session).isNotNull() - assertThat(session!!.events).isNotEmpty() - - // And they survive a database reopen. - sessionService.close() - val reopened = RoomSessionService.fromContext(context, databaseName = TEST_DB_NAME) - try { - val persisted = reopened.getSession(key) - assertThat(persisted).isNotNull() - assertThat(persisted!!.events.map { it.id }) - .containsAtLeastElementsIn(session.events.map { it.id }) - } finally { - reopened.close() - } - } - private companion object { const val APP_NAME = "RoomSessionServiceInstrumentedTestApp" const val TEST_DB_NAME = "room_session_service_instrumented_test.db" - const val QUESTION = "Tell me something about planet Earth." } } diff --git a/examples/android/build.gradle.kts b/examples/android/build.gradle.kts index 6deabf06..f1104273 100644 --- a/examples/android/build.gradle.kts +++ b/examples/android/build.gradle.kts @@ -113,6 +113,7 @@ run { dependencies { implementation(project(":google-adk-kotlin-core")) implementation(project(":google-adk-kotlin-firebase")) + implementation(project(":google-adk-kotlin-mlkit")) implementation(platform(libs.google.firebase.platform)) implementation(libs.google.firebase.ai) implementation(libs.google.mlkit.genai.prompt) diff --git a/examples/android/src/main/kotlin/com/google/adk/kt/examples/android/mlkitchat/MlKitChatActivity.kt b/examples/android/src/main/kotlin/com/google/adk/kt/examples/android/mlkitchat/MlKitChatActivity.kt index c646a378..0f5b6d1c 100644 --- a/examples/android/src/main/kotlin/com/google/adk/kt/examples/android/mlkitchat/MlKitChatActivity.kt +++ b/examples/android/src/main/kotlin/com/google/adk/kt/examples/android/mlkitchat/MlKitChatActivity.kt @@ -32,13 +32,13 @@ import com.google.adk.kt.agents.StreamingMode import com.google.adk.kt.examples.android.common.ScopedExampleActivity import com.google.adk.kt.examples.android.common.foldTextParts import com.google.adk.kt.examples.android.common.setExampleContentView -import com.google.adk.kt.models.mlkit.GenaiPrompt +import com.google.adk.kt.mlkit.GenaiPrompt +import com.google.adk.kt.mlkit.GenerativeModelHelpers import com.google.adk.kt.runners.InMemoryRunner import com.google.adk.kt.sessions.InMemorySessionService import com.google.adk.kt.types.Content import com.google.adk.kt.types.Part import com.google.adk.kt.types.Role -import com.google.adk.kt.utils.mlkit.GenerativeModelHelpers import kotlinx.coroutines.launch /** diff --git a/examples/android/src/main/kotlin/com/google/adk/kt/examples/android/roomsession/RoomSessionActivity.kt b/examples/android/src/main/kotlin/com/google/adk/kt/examples/android/roomsession/RoomSessionActivity.kt index 7939793d..62321d5d 100644 --- a/examples/android/src/main/kotlin/com/google/adk/kt/examples/android/roomsession/RoomSessionActivity.kt +++ b/examples/android/src/main/kotlin/com/google/adk/kt/examples/android/roomsession/RoomSessionActivity.kt @@ -28,14 +28,14 @@ import com.google.adk.kt.agents.LlmAgent import com.google.adk.kt.examples.android.common.ScopedExampleActivity import com.google.adk.kt.examples.android.common.foldTextParts import com.google.adk.kt.examples.android.common.setExampleContentView -import com.google.adk.kt.models.mlkit.GenaiPrompt +import com.google.adk.kt.mlkit.GenaiPrompt +import com.google.adk.kt.mlkit.GenerativeModelHelpers import com.google.adk.kt.runners.InMemoryRunner import com.google.adk.kt.sessions.SessionKey import com.google.adk.kt.sessions.room.RoomSessionService import com.google.adk.kt.types.Content import com.google.adk.kt.types.Part import com.google.adk.kt.types.Role -import com.google.adk.kt.utils.mlkit.GenerativeModelHelpers import kotlinx.coroutines.launch /** diff --git a/examples/android/src/main/kotlin/com/google/adk/kt/examples/android/skillsassetsource/WizardApprenticeAgent.kt b/examples/android/src/main/kotlin/com/google/adk/kt/examples/android/skillsassetsource/WizardApprenticeAgent.kt index eab78311..9add47e6 100644 --- a/examples/android/src/main/kotlin/com/google/adk/kt/examples/android/skillsassetsource/WizardApprenticeAgent.kt +++ b/examples/android/src/main/kotlin/com/google/adk/kt/examples/android/skillsassetsource/WizardApprenticeAgent.kt @@ -19,10 +19,10 @@ package com.google.adk.kt.examples.android.skillsassetsource import android.content.Context import com.google.adk.kt.agents.Instruction import com.google.adk.kt.agents.LlmAgent -import com.google.adk.kt.models.mlkit.GenaiPrompt +import com.google.adk.kt.mlkit.GenaiPrompt +import com.google.adk.kt.mlkit.GenerativeModelHelpers import com.google.adk.kt.skills.AssetSkillSource import com.google.adk.kt.tools.SkillToolset -import com.google.adk.kt.utils.mlkit.GenerativeModelHelpers /** * Builds the "wizard's apprentice" [LlmAgent] used by [SkillsAssetSourceActivity]. diff --git a/mlkit/build.gradle.kts b/mlkit/build.gradle.kts new file mode 100644 index 00000000..0b0b5475 --- /dev/null +++ b/mlkit/build.gradle.kts @@ -0,0 +1,121 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + // Kotlin is compiled by AGP's built-in Kotlin support (no kotlin-android plugin). + id("com.android.library") + id("maven-publish") +} + +// AGP's built-in Kotlin doesn't apply the kotlin.jvm/multiplatform plugins that +// the root project pins coreLibrariesVersion on, so set it here too. Keeps the +// published AAR on the 2.1 stdlib and consumable by Kotlin 2.1 projects. +kotlin { coreLibrariesVersion = rootProject.extra["kotlinCoreLibrariesVersion"] as String } + +dependencies { + implementation(project(":google-adk-kotlin-core")) + implementation(libs.google.mlkit.genai.prompt) + // GenaiPromptConversions uses androidx.core.net.toUri to turn file URIs into ImageParts. + implementation(libs.androidx.core) + + testImplementation(libs.kotlin.test.junit) + testImplementation(libs.androidx.test.core) + testImplementation(libs.androidx.test.ext.junit) + testImplementation(libs.androidx.test.runner) + testImplementation(libs.mockito.kotlin) + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.google.truth) + testImplementation(libs.robolectric) + + // Instrumented tests drive the real on-device Gemini Nano model on a physical device. + androidTestImplementation(libs.androidx.compose.ui.test.junit4) + androidTestImplementation(libs.androidx.compose.ui.test.manifest) + // Pull espresso-core explicitly so the whole androidx.test stack resolves to the newer version + // that compose ui-test-junit4 alone does not force; older espresso crashes on newer Android with + // NoSuchMethodException android.hardware.input.InputManager.getInstance during + // createComposeRule(). + androidTestImplementation(libs.androidx.test.espresso.core) + androidTestImplementation(libs.androidx.test.ext.junit) + androidTestImplementation(libs.androidx.test.rules) + androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.google.truth) + androidTestImplementation(libs.kotlinx.coroutines.test) +} + +android { + namespace = "com.google.adk.mlkit" + + testOptions { + unitTests { + isReturnDefaultValues = true + isIncludeAndroidResources = true + all { it.systemProperty("robolectric.logging", "stderr") } + } + } + + // Publishes the Android `release` variant as a single AAR with sources and + // javadoc jars. AGP's `withJavadocJar()` runs Gradle's `javadoc` task, + // which doesn't understand `.kt` sources, so the resulting jar is + // effectively empty - but it still satisfies Maven Central's per-module + // requirement. Replacing with Dokka HTML would require building the jar + // manually and attaching it to the AGP-created publication after + // evaluation; left as a follow-up. + publishing { + singleVariant("release") { + withSourcesJar() + withJavadocJar() + } + } + + // The ML Kit GenAI / Gemini Nano stack pulls in androidx.core:core:1.16.0, + // which requires compileSdk >= 35. Pin to 36 (matching the other Android + // modules) even though the repo-wide default stays lower for the pure-JVM + // publications. + compileSdk { version = release(36) { minorApiLevel = 1 } } + + defaultConfig { + minSdk = 26 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } +} + +// AGP registers a software component named `release` (created by the +// `android { publishing { singleVariant("release") { ... } } }` block above), +// but it does NOT auto-create a `MavenPublication` for it — that's our +// responsibility. We have to do this inside `afterEvaluate` because the +// component itself is only registered once the Android extension finishes +// configuring. POM metadata and GPG signing are configured in the root +// build.gradle.kts; the root script also intentionally skips attaching the +// Dokka javadoc jar to this publication because AGP's `withJavadocJar()` +// above already attaches one. +afterEvaluate { + publishing { + publications { + create("release") { + from(components["release"]) + artifactId = "google-adk-kotlin-mlkit-android" + } + } + } +} diff --git a/core/src/androidDeviceTest/kotlin/com/google/adk/kt/models/mlkit/GenaiPromptInstrumentedTest.kt b/mlkit/src/androidTest/kotlin/com/google/adk/kt/mlkit/GenaiPromptInstrumentedTest.kt similarity index 98% rename from core/src/androidDeviceTest/kotlin/com/google/adk/kt/models/mlkit/GenaiPromptInstrumentedTest.kt rename to mlkit/src/androidTest/kotlin/com/google/adk/kt/mlkit/GenaiPromptInstrumentedTest.kt index 851252bf..9cf5de33 100644 --- a/core/src/androidDeviceTest/kotlin/com/google/adk/kt/models/mlkit/GenaiPromptInstrumentedTest.kt +++ b/mlkit/src/androidTest/kotlin/com/google/adk/kt/mlkit/GenaiPromptInstrumentedTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.adk.kt.models.mlkit +package com.google.adk.kt.mlkit import android.util.Log import androidx.compose.ui.test.junit4.v2.createComposeRule @@ -30,7 +30,6 @@ import com.google.adk.kt.types.Blob import com.google.adk.kt.types.Content import com.google.adk.kt.types.Part import com.google.adk.kt.types.Role -import com.google.adk.kt.utils.mlkit.GenerativeModelHelpers import com.google.common.truth.Truth.assertThat import com.google.mlkit.genai.prompt.ModelConfig import com.google.mlkit.genai.prompt.ModelPreference diff --git a/mlkit/src/androidTest/kotlin/com/google/adk/kt/mlkit/RoomSessionGenaiPromptInstrumentedTest.kt b/mlkit/src/androidTest/kotlin/com/google/adk/kt/mlkit/RoomSessionGenaiPromptInstrumentedTest.kt new file mode 100644 index 00000000..e6e80deb --- /dev/null +++ b/mlkit/src/androidTest/kotlin/com/google/adk/kt/mlkit/RoomSessionGenaiPromptInstrumentedTest.kt @@ -0,0 +1,132 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.adk.kt.mlkit + +import android.content.Context +import androidx.compose.ui.test.junit4.v2.createComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.google.adk.kt.agents.Instruction +import com.google.adk.kt.agents.LlmAgent +import com.google.adk.kt.agents.RunConfig +import com.google.adk.kt.agents.StreamingMode +import com.google.adk.kt.runners.InMemoryRunner +import com.google.adk.kt.sessions.SessionKey +import com.google.adk.kt.sessions.room.RoomSessionService +import com.google.adk.kt.types.Content +import com.google.adk.kt.types.Part +import com.google.adk.kt.types.Role +import com.google.common.truth.Truth.assertThat +import com.google.mlkit.genai.prompt.ModelConfig +import com.google.mlkit.genai.prompt.ModelPreference +import com.google.mlkit.genai.prompt.ModelReleaseStage +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * On-device instrumented test that drives a real Gemini Nano turn through [GenaiPrompt] and an + * [InMemoryRunner] backed by the Room-persisted [RoomSessionService], asserting the turn's events + * survive a database reopen. Requires a device with AI Core / Gemini Nano. + */ +@RunWith(AndroidJUnit4::class) +class RoomSessionGenaiPromptInstrumentedTest { + + // Launches a ComponentActivity to keep the test app in the foreground; AICore rejects on-device + // GenAI calls made from the background (BACKGROUND_USE_BLOCKED). + @get:Rule val rule = createComposeRule() + + private lateinit var context: Context + private lateinit var sessionService: RoomSessionService + + @Before + fun setUp() { + context = InstrumentationRegistry.getInstrumentation().targetContext + context.deleteDatabase(TEST_DB_NAME) + sessionService = RoomSessionService.fromContext(context, databaseName = TEST_DB_NAME) + } + + @After + fun tearDown() { + runCatching { sessionService.close() } + context.deleteDatabase(TEST_DB_NAME) + } + + @Test + fun runTurn_withGenaiPrompt_persistsEventsAcrossReopen(): Unit = runBlocking { + // initGenerativeModel is suspend and downloads Gemini Nano if needed. + val generativeModel = GenerativeModelHelpers.initGenerativeModel { + modelConfig = + ModelConfig.builder() + .apply { + releaseStage = ModelReleaseStage.STABLE + preference = ModelPreference.FULL + } + .build() + } + val agent = + LlmAgent( + name = "room_session_agent", + model = GenaiPrompt.create(generativeModel, name = "gemini-nano"), + instruction = Instruction("You are a helpful assistant. Answer in one short sentence."), + ) + val runner = InMemoryRunner(agent = agent, appName = APP_NAME, sessionService = sessionService) + + val userId = "user-e2e" + val sessionId = "session-e2e" + val events = + runner + .runAsync( + userId = userId, + sessionId = sessionId, + newMessage = Content(role = Role.USER, parts = listOf(Part(text = QUESTION))), + runConfig = RunConfig(streamingMode = StreamingMode.NONE), + ) + .toList() + + assertThat(events).isNotEmpty() + assertThat(events.mapNotNull { it.errorCode }).isEmpty() + + // The turn's events were persisted to the Room-backed session. + val key = SessionKey(APP_NAME, userId, sessionId) + val session = sessionService.getSession(key) + assertThat(session).isNotNull() + assertThat(session!!.events).isNotEmpty() + + // And they survive a database reopen. + sessionService.close() + val reopened = RoomSessionService.fromContext(context, databaseName = TEST_DB_NAME) + try { + val persisted = reopened.getSession(key) + assertThat(persisted).isNotNull() + assertThat(persisted!!.events.map { it.id }) + .containsAtLeastElementsIn(session.events.map { it.id }) + } finally { + reopened.close() + } + } + + private companion object { + const val APP_NAME = "RoomSessionGenaiPromptInstrumentedTestApp" + const val TEST_DB_NAME = "room_session_genai_prompt_instrumented_test.db" + const val QUESTION = "Tell me something about planet Earth." + } +} diff --git a/mlkit/src/main/AndroidManifest.xml b/mlkit/src/main/AndroidManifest.xml new file mode 100644 index 00000000..82bf81af --- /dev/null +++ b/mlkit/src/main/AndroidManifest.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/core/src/androidHostTest/assets/test-image.png b/mlkit/src/main/assets/test-image.png similarity index 100% rename from core/src/androidHostTest/assets/test-image.png rename to mlkit/src/main/assets/test-image.png diff --git a/core/src/androidMain/kotlin/com/google/adk/kt/models/mlkit/GenaiPrompt.kt b/mlkit/src/main/kotlin/com/google/adk/kt/mlkit/GenaiPrompt.kt similarity index 90% rename from core/src/androidMain/kotlin/com/google/adk/kt/models/mlkit/GenaiPrompt.kt rename to mlkit/src/main/kotlin/com/google/adk/kt/mlkit/GenaiPrompt.kt index 81f02b71..dd755b2c 100644 --- a/core/src/androidMain/kotlin/com/google/adk/kt/models/mlkit/GenaiPrompt.kt +++ b/mlkit/src/main/kotlin/com/google/adk/kt/mlkit/GenaiPrompt.kt @@ -14,17 +14,15 @@ * limitations under the License. */ -package com.google.adk.kt.models.mlkit +package com.google.adk.kt.mlkit import com.google.adk.kt.logging.LoggerFactory +import com.google.adk.kt.mlkit.GenaiPromptConversions.toGenerateContentRequest +import com.google.adk.kt.mlkit.GenaiPromptConversions.toLlmResponse +import com.google.adk.kt.mlkit.GenaiPromptTracing.format import com.google.adk.kt.models.LlmRequest import com.google.adk.kt.models.LlmResponse import com.google.adk.kt.models.Model -import com.google.adk.kt.models.mlkit.GenaiPromptTracing.format -import com.google.adk.kt.utils.mlkit.GenaiPromptConversions.toGenerateContentRequest -import com.google.adk.kt.utils.mlkit.GenaiPromptConversions.toLlmResponse -import com.google.adk.kt.utils.mlkit.GenerateContentResponseAggregator -import com.google.adk.kt.utils.mlkit.toAggregatedResponse import com.google.mlkit.genai.prompt.GenerativeModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow diff --git a/core/src/androidMain/kotlin/com/google/adk/kt/utils/mlkit/GenaiPromptConversions.kt b/mlkit/src/main/kotlin/com/google/adk/kt/mlkit/GenaiPromptConversions.kt similarity index 99% rename from core/src/androidMain/kotlin/com/google/adk/kt/utils/mlkit/GenaiPromptConversions.kt rename to mlkit/src/main/kotlin/com/google/adk/kt/mlkit/GenaiPromptConversions.kt index c79c0406..a7c64868 100644 --- a/core/src/androidMain/kotlin/com/google/adk/kt/utils/mlkit/GenaiPromptConversions.kt +++ b/mlkit/src/main/kotlin/com/google/adk/kt/mlkit/GenaiPromptConversions.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.adk.kt.utils.mlkit +package com.google.adk.kt.mlkit import androidx.core.net.toUri import com.google.adk.kt.logging.LoggerFactory diff --git a/core/src/androidMain/kotlin/com/google/adk/kt/models/mlkit/GenaiPromptTracing.kt b/mlkit/src/main/kotlin/com/google/adk/kt/mlkit/GenaiPromptTracing.kt similarity index 98% rename from core/src/androidMain/kotlin/com/google/adk/kt/models/mlkit/GenaiPromptTracing.kt rename to mlkit/src/main/kotlin/com/google/adk/kt/mlkit/GenaiPromptTracing.kt index 1c29d92a..58f8cd70 100644 --- a/core/src/androidMain/kotlin/com/google/adk/kt/models/mlkit/GenaiPromptTracing.kt +++ b/mlkit/src/main/kotlin/com/google/adk/kt/mlkit/GenaiPromptTracing.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.adk.kt.models.mlkit +package com.google.adk.kt.mlkit import com.google.adk.kt.models.LlmRequest import com.google.adk.kt.models.LlmResponse @@ -22,7 +22,6 @@ import com.google.adk.kt.types.Content import com.google.adk.kt.types.FunctionCall import com.google.adk.kt.types.FunctionResponse import com.google.adk.kt.types.Part -import com.google.adk.kt.utils.mlkit.AggregatedResponse import com.google.mlkit.genai.prompt.GenerateContentRequest import com.google.mlkit.genai.prompt.GenerateContentResponse import com.google.mlkit.genai.prompt.ImagePart diff --git a/core/src/androidMain/kotlin/com/google/adk/kt/utils/mlkit/GenerateContentResponseAggregator.kt b/mlkit/src/main/kotlin/com/google/adk/kt/mlkit/GenerateContentResponseAggregator.kt similarity index 98% rename from core/src/androidMain/kotlin/com/google/adk/kt/utils/mlkit/GenerateContentResponseAggregator.kt rename to mlkit/src/main/kotlin/com/google/adk/kt/mlkit/GenerateContentResponseAggregator.kt index b18577f9..3acae496 100644 --- a/core/src/androidMain/kotlin/com/google/adk/kt/utils/mlkit/GenerateContentResponseAggregator.kt +++ b/mlkit/src/main/kotlin/com/google/adk/kt/mlkit/GenerateContentResponseAggregator.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.adk.kt.utils.mlkit +package com.google.adk.kt.mlkit import com.google.mlkit.genai.prompt.Candidate import com.google.mlkit.genai.prompt.GenerateContentResponse diff --git a/core/src/androidMain/kotlin/com/google/adk/kt/utils/mlkit/GenerativeModelHelpers.kt b/mlkit/src/main/kotlin/com/google/adk/kt/mlkit/GenerativeModelHelpers.kt similarity index 99% rename from core/src/androidMain/kotlin/com/google/adk/kt/utils/mlkit/GenerativeModelHelpers.kt rename to mlkit/src/main/kotlin/com/google/adk/kt/mlkit/GenerativeModelHelpers.kt index 9a140da0..3409e7aa 100644 --- a/core/src/androidMain/kotlin/com/google/adk/kt/utils/mlkit/GenerativeModelHelpers.kt +++ b/mlkit/src/main/kotlin/com/google/adk/kt/mlkit/GenerativeModelHelpers.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.adk.kt.utils.mlkit +package com.google.adk.kt.mlkit import com.google.adk.kt.logging.LoggerFactory import com.google.mlkit.genai.common.DownloadStatus diff --git a/mlkit/src/main/kotlin/com/google/adk/kt/models/mlkit/GenaiPrompt.kt b/mlkit/src/main/kotlin/com/google/adk/kt/models/mlkit/GenaiPrompt.kt new file mode 100644 index 00000000..c9fe5e08 --- /dev/null +++ b/mlkit/src/main/kotlin/com/google/adk/kt/models/mlkit/GenaiPrompt.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.adk.kt.models.mlkit + +/** + * Backwards-compatible alias for [com.google.adk.kt.mlkit.GenaiPrompt], which moved to the + * `com.google.adk.kt.mlkit` package. + */ +@Deprecated( + "Moved to com.google.adk.kt.mlkit.GenaiPrompt.", + ReplaceWith("GenaiPrompt", "com.google.adk.kt.mlkit.GenaiPrompt"), +) +typealias GenaiPrompt = com.google.adk.kt.mlkit.GenaiPrompt diff --git a/mlkit/src/main/kotlin/com/google/adk/kt/utils/mlkit/GenerativeModelHelpers.kt b/mlkit/src/main/kotlin/com/google/adk/kt/utils/mlkit/GenerativeModelHelpers.kt new file mode 100644 index 00000000..59121c82 --- /dev/null +++ b/mlkit/src/main/kotlin/com/google/adk/kt/utils/mlkit/GenerativeModelHelpers.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.adk.kt.utils.mlkit + +/** + * Backwards-compatible alias for [com.google.adk.kt.mlkit.GenerativeModelHelpers], which moved to + * the `com.google.adk.kt.mlkit` package. + */ +@Deprecated( + "Moved to com.google.adk.kt.mlkit.GenerativeModelHelpers.", + ReplaceWith("GenerativeModelHelpers", "com.google.adk.kt.mlkit.GenerativeModelHelpers"), +) +typealias GenerativeModelHelpers = com.google.adk.kt.mlkit.GenerativeModelHelpers diff --git a/core/src/androidHostTest/kotlin/com/google/adk/kt/utils/mlkit/GenaiPromptConversionsTest.kt b/mlkit/src/test/kotlin/com/google/adk/kt/mlkit/GenaiPromptConversionsTest.kt similarity index 98% rename from core/src/androidHostTest/kotlin/com/google/adk/kt/utils/mlkit/GenaiPromptConversionsTest.kt rename to mlkit/src/test/kotlin/com/google/adk/kt/mlkit/GenaiPromptConversionsTest.kt index 81dff41d..76b93058 100644 --- a/core/src/androidHostTest/kotlin/com/google/adk/kt/utils/mlkit/GenaiPromptConversionsTest.kt +++ b/mlkit/src/test/kotlin/com/google/adk/kt/mlkit/GenaiPromptConversionsTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.adk.kt.utils.mlkit +package com.google.adk.kt.mlkit import android.content.Context import android.graphics.Bitmap @@ -21,6 +21,8 @@ import android.net.Uri import androidx.core.net.toUri import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.adk.kt.mlkit.GenaiPromptConversions.toGenerateContentRequest +import com.google.adk.kt.mlkit.GenaiPromptConversions.toLlmResponse import com.google.adk.kt.models.LlmRequest import com.google.adk.kt.types.Blob import com.google.adk.kt.types.Content @@ -28,8 +30,6 @@ import com.google.adk.kt.types.FileData import com.google.adk.kt.types.FinishReason import com.google.adk.kt.types.GenerateContentConfig import com.google.adk.kt.types.Part -import com.google.adk.kt.utils.mlkit.GenaiPromptConversions.toGenerateContentRequest -import com.google.adk.kt.utils.mlkit.GenaiPromptConversions.toLlmResponse import com.google.common.truth.Truth.assertThat import com.google.mlkit.common.sdkinternal.MlKitContext import com.google.mlkit.genai.prompt.Candidate.FinishReason as MlKitFinishReason diff --git a/core/src/androidHostTest/kotlin/com/google/adk/kt/models/mlkit/GenaiPromptHostTest.kt b/mlkit/src/test/kotlin/com/google/adk/kt/mlkit/GenaiPromptHostTest.kt similarity index 99% rename from core/src/androidHostTest/kotlin/com/google/adk/kt/models/mlkit/GenaiPromptHostTest.kt rename to mlkit/src/test/kotlin/com/google/adk/kt/mlkit/GenaiPromptHostTest.kt index d2111fb7..ab072507 100644 --- a/core/src/androidHostTest/kotlin/com/google/adk/kt/models/mlkit/GenaiPromptHostTest.kt +++ b/mlkit/src/test/kotlin/com/google/adk/kt/mlkit/GenaiPromptHostTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.adk.kt.models.mlkit +package com.google.adk.kt.mlkit import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.adk.kt.agents.Instruction diff --git a/core/src/androidHostTest/kotlin/com/google/adk/kt/models/mlkit/GenaiPromptTracingTest.kt b/mlkit/src/test/kotlin/com/google/adk/kt/mlkit/GenaiPromptTracingTest.kt similarity index 99% rename from core/src/androidHostTest/kotlin/com/google/adk/kt/models/mlkit/GenaiPromptTracingTest.kt rename to mlkit/src/test/kotlin/com/google/adk/kt/mlkit/GenaiPromptTracingTest.kt index b4eac485..dc4f7d92 100644 --- a/core/src/androidHostTest/kotlin/com/google/adk/kt/models/mlkit/GenaiPromptTracingTest.kt +++ b/mlkit/src/test/kotlin/com/google/adk/kt/mlkit/GenaiPromptTracingTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.adk.kt.models.mlkit +package com.google.adk.kt.mlkit import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.adk.kt.models.LlmRequest diff --git a/core/src/androidHostTest/kotlin/com/google/adk/kt/utils/mlkit/GenerateContentResponseAggregatorTest.kt b/mlkit/src/test/kotlin/com/google/adk/kt/mlkit/GenerateContentResponseAggregatorTest.kt similarity index 99% rename from core/src/androidHostTest/kotlin/com/google/adk/kt/utils/mlkit/GenerateContentResponseAggregatorTest.kt rename to mlkit/src/test/kotlin/com/google/adk/kt/mlkit/GenerateContentResponseAggregatorTest.kt index 372d5011..a8a77189 100644 --- a/core/src/androidHostTest/kotlin/com/google/adk/kt/utils/mlkit/GenerateContentResponseAggregatorTest.kt +++ b/mlkit/src/test/kotlin/com/google/adk/kt/mlkit/GenerateContentResponseAggregatorTest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.adk.kt.utils.mlkit +package com.google.adk.kt.mlkit import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat diff --git a/core/src/androidHostTest/kotlin/com/google/adk/kt/utils/mlkit/GenerativeModelHelpersTest.kt b/mlkit/src/test/kotlin/com/google/adk/kt/mlkit/GenerativeModelHelpersTest.kt similarity index 98% rename from core/src/androidHostTest/kotlin/com/google/adk/kt/utils/mlkit/GenerativeModelHelpersTest.kt rename to mlkit/src/test/kotlin/com/google/adk/kt/mlkit/GenerativeModelHelpersTest.kt index ba47f0d9..ce0b2515 100644 --- a/core/src/androidHostTest/kotlin/com/google/adk/kt/utils/mlkit/GenerativeModelHelpersTest.kt +++ b/mlkit/src/test/kotlin/com/google/adk/kt/mlkit/GenerativeModelHelpersTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.adk.kt.utils.mlkit +package com.google.adk.kt.mlkit import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.mlkit.genai.common.DownloadStatus diff --git a/mlkit/src/test/resources/robolectric.properties b/mlkit/src/test/resources/robolectric.properties new file mode 100644 index 00000000..4cd20c51 --- /dev/null +++ b/mlkit/src/test/resources/robolectric.properties @@ -0,0 +1,3 @@ +# Robolectric's SDK 36 sandbox requires JDK 21+; pin host tests to SDK 34 so they +# also run on the Java 17 CI matrix. compileSdk stays 36 for the library itself. +sdk=34 diff --git a/settings.gradle.kts b/settings.gradle.kts index dda5f8e1..2adb2aae 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -50,6 +50,10 @@ include(":google-adk-kotlin-firebase") project(":google-adk-kotlin-firebase").projectDir = file("firebase") +include(":google-adk-kotlin-mlkit") + +project(":google-adk-kotlin-mlkit").projectDir = file("mlkit") + include(":google-adk-kotlin-examples") project(":google-adk-kotlin-examples").projectDir = file("examples")