Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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."
}
}
1 change: 1 addition & 0 deletions examples/android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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].
Expand Down
121 changes: 121 additions & 0 deletions mlkit/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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<MavenPublication>("release") {
from(components["release"])
artifactId = "google-adk-kotlin-mlkit-android"
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading
Loading