From 8e09662ec4cabde0a6ecfbe0a6577c763301d167 Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Mon, 20 Jul 2026 02:10:03 -0700 Subject: [PATCH] feat: add a Firebase AI example to the Android examples app Adds a Firebase AI (Gemini) chat example to the consolidated Android examples app: an ADK LlmAgent backed by Firebase AI that handles plain chat and tool calling. PiperOrigin-RevId: 950691153 --- build.gradle.kts | 1 + examples/android/.gitignore | 6 + examples/android/README.md | 94 ++++++++++ examples/android/build.gradle.kts | 89 ++++++++-- examples/android/src/main/AndroidManifest.xml | 25 +++ .../kt/examples/android/common/EventText.kt | 13 +- .../android/firebase/FirebaseAppResolver.kt | 100 +++++++++++ .../android/firebase/FirebaseChatActivity.kt | 167 ++++++++++++++++++ .../android/firebase/FirebaseChatAgent.kt | 57 ++++++ .../examples/android/firebase/WeatherTools.kt | 39 ++++ .../kt/examples/android/home/HomeActivity.kt | 7 + gradle/libs.versions.toml | 2 + 12 files changed, 587 insertions(+), 13 deletions(-) create mode 100644 examples/android/.gitignore create mode 100644 examples/android/README.md create mode 100644 examples/android/src/main/kotlin/com/google/adk/kt/examples/android/firebase/FirebaseAppResolver.kt create mode 100644 examples/android/src/main/kotlin/com/google/adk/kt/examples/android/firebase/FirebaseChatActivity.kt create mode 100644 examples/android/src/main/kotlin/com/google/adk/kt/examples/android/firebase/FirebaseChatAgent.kt create mode 100644 examples/android/src/main/kotlin/com/google/adk/kt/examples/android/firebase/WeatherTools.kt diff --git a/build.gradle.kts b/build.gradle.kts index 5d187bfa..a1157c7f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,6 +25,7 @@ plugins { id("com.google.cloud.artifactregistry.gradle-plugin") version "2.2.4" apply false kotlin("plugin.serialization") version "2.3.21" apply false alias(libs.plugins.gradle.test.retry) apply false + alias(libs.plugins.google.services) apply false } val jdkVersion = providers.gradleProperty("jdkVersion").getOrElse("17").toInt() diff --git a/examples/android/.gitignore b/examples/android/.gitignore new file mode 100644 index 00000000..64a8f39b --- /dev/null +++ b/examples/android/.gitignore @@ -0,0 +1,6 @@ +# Firebase example config. Ignored in THIS sample so forks point at their own Firebase project +# instead of ours -- the file just identifies a specific project. Its contents are not secret (the +# Firebase config/API key is public by design and ships inside the APK), so your own app is free to +# commit it if you prefer. Drop your own file here (see README.md); the google-services Gradle +# plugin picks it up. +google-services.json diff --git a/examples/android/README.md b/examples/android/README.md new file mode 100644 index 00000000..9eb3e607 --- /dev/null +++ b/examples/android/README.md @@ -0,0 +1,94 @@ +# ADK Kotlin — Android examples + +A single installable Android app that collects the ADK Kotlin Android examples +behind a launcher menu +([`HomeActivity`](src/main/kotlin/com/google/adk/kt/examples/android/home/HomeActivity.kt)). +Each menu entry opens one self-contained example `Activity`: + +- **Room session** — an on-device agent whose conversation is persisted across + restarts by the Room-backed session service. +- **Skills (AssetSkillSource)** — an agent whose `SkillToolset` reads skill + definitions from the APK's `assets/skills/...` tree. +- **ML Kit chat** — a multi-turn chat with an on-device Gemini Nano agent, + with a streaming toggle. +- **Firebase AI** — a chat backed by the cloud Firebase AI (Gemini) model from + the [`:google-adk-kotlin-firebase`](../../firebase) module, demonstrating + both plain chat and tool calling (via + [`WeatherTools.kt`](src/main/kotlin/com/google/adk/kt/examples/android/firebase/WeatherTools.kt)). + +The first three run fully on-device through ML Kit's Gemini Nano, so they need +no API key or network (the first run may download Gemini Nano). The **Firebase +AI** example calls the cloud Firebase backend, so it needs a Firebase +configuration and network access — see +[Configure Firebase](#configure-firebase-only-for-the-firebase-ai-example) +below. + +## Build & run + +With a device or emulator connected: + +```shell +./gradlew :google-adk-kotlin-examples-android:installDebug +``` + +Then launch **"ADK Android Examples"** from the launcher and pick an example. + +## Configure Firebase (only for the Firebase AI example) + +The Firebase AI example needs to know which Firebase project to talk to; the +other examples don't need any of this. Two ways, in order of preference: + +### 1. `google-services.json` (standard Firebase setup — recommended) + +This mirrors what a normal Firebase Android developer does. + +1. In the [Firebase console](https://console.firebase.google.com/), open your + project (or create one) and enable the **Firebase AI Logic** / Gemini API. +2. Register an **Android app** with the package name + **`com.google.adk.kt.examples.android`** (this is the `applicationId` in + [`build.gradle.kts`](build.gradle.kts); change both if you prefer your own). +3. Download the generated `google-services.json` and place it in **this + directory** (`examples/android/google-services.json`). + +This file just points the app at a specific Firebase project; its contents are +**not secret** — the Firebase config/API key is +[public by design](https://firebase.google.com/docs/projects/api-keys) and ships +inside the APK anyway, so a normal app can commit it (and often does, especially +in a private repo). It's **git-ignored here only** so that forks of this sample +use their own Firebase project instead of ours. When present, the +`com.google.gms.google-services` Gradle plugin is applied automatically and +initializes the default `FirebaseApp` for you. + +### 2. Build-time Firebase config (fallback) + +If you don't have a `google-services.json`, supply the three values directly and +they are baked into the APK's manifest at build time. Pass them as Gradle +properties: + +```shell +./gradlew :google-adk-kotlin-examples-android:installDebug \ + -PFIREBASE_API_KEY=your_api_key \ + -PFIREBASE_APP_ID=your_app_id \ + -PFIREBASE_PROJECT_ID=your_project_id +``` + +or export the matching `FIREBASE_API_KEY` / `FIREBASE_APP_ID` / +`FIREBASE_PROJECT_ID` environment variables before building. (These are read at +build time, not from the device's environment.) + +If neither method provides a configuration, the Gradle build prints a warning, +and the Firebase AI example starts but shows a message explaining what to add +instead of calling Firebase with a blank config. The other examples are +unaffected. + +To change the model, edit `MODEL_NAME` in +[`FirebaseChatAgent.kt`](src/main/kotlin/com/google/adk/kt/examples/android/firebase/FirebaseChatAgent.kt). + +> Note: the `FIREBASE_*` values (like those in `google-services.json`) are +> project *identifiers*, not secrets — they're public by design and always end +> up inside the APK. What you must keep out of the app and the repo is a +> genuinely secret key, such as a Gemini Developer API key or an Admin SDK +> service-account key; this sample uses neither (it talks to the model through +> Firebase AI Logic). + +[`LlmAgent`]: ../../core/src/main/kotlin/com/google/adk/kt/agents/LlmAgent.kt diff --git a/examples/android/build.gradle.kts b/examples/android/build.gradle.kts index d15a51a4..6deabf06 100644 --- a/examples/android/build.gradle.kts +++ b/examples/android/build.gradle.kts @@ -17,29 +17,57 @@ plugins { // Kotlin is compiled by AGP's built-in Kotlin support (no kotlin-android plugin). id("com.android.application") + // Generates the `@Tool` FunctionTools used by the Firebase example + // (WeatherTools.generatedTools()). + alias(libs.plugins.ksp) } -// This sample transitively depends on androidx.core:core:1.16.0 (via the ML Kit GenAI / Gemini Nano -// stack), which requires compileSdk >= 35. Pin this example to at least 35 so it builds on its own -// without raising the repo-wide default (kept lower for the published libraries). core needs 35 the -// same way for its instrumentation tests (passed there via -PandroidCompileSdk=35). -val exampleCompileSdk = maxOf(35, rootProject.extra["androidCompileSdk"] as Int) +// Standard Firebase developer setup for the Firebase example: the `com.google.gms.google-services` +// plugin reads a `google-services.json` from this module's root and generates the default +// FirebaseApp configuration so `FirebaseApp.getInstance()` works with zero code. The plugin is +// applied only when a developer has dropped their own file in. When it is absent the app still +// builds and the Firebase example falls back to the FIREBASE_* environment variables (see +// FirebaseChatActivity). Read README.md for setup. +if (file("google-services.json").exists()) { + apply(plugin = "com.google.gms.google-services") +} + +// The build-time fallback config keys for the Firebase example, read as Gradle properties or +// environment variables. Shared by the manifest-placeholder loop and the "no configuration" +// diagnostic below. +val firebaseConfigKeys = listOf("FIREBASE_API_KEY", "FIREBASE_APP_ID", "FIREBASE_PROJECT_ID") android { namespace = "com.google.adk.kt.examples.android" - compileSdk = exampleCompileSdk + + // compileSdk 36.1: the Firebase example depends on the Firebase AI SDK (via + // :google-adk-kotlin-firebase), which is built against 36.1, and the ML Kit GenAI / Gemini Nano + // stack pulls in androidx.core:core:1.16.0, which needs compileSdk >= 35. 36.1 satisfies both. + compileSdk { version = release(36) { minorApiLevel = 1 } } defaultConfig { applicationId = "com.google.adk.kt.examples.android" minSdk = rootProject.extra["androidMinSdk"] as Int - targetSdk = exampleCompileSdk + targetSdk = 36 versionCode = 1 versionName = "0.1.0" + + // Fallback Firebase-config path for the Firebase example (see FirebaseChatActivity): when no + // google-services.json is present, the Firebase config can instead be baked into the APK at + // build time as manifest metadata, sourced from Gradle properties (-PFIREBASE_API_KEY=...) or + // the matching environment variables. + for (name in firebaseConfigKeys) { + manifestPlaceholders[name] = + providers + .gradleProperty(name) + .orElse(providers.environmentVariable(name)) + .getOrElse("\${$name}") + } } - // The genai/auth transitive dependencies each ship a META-INF/INDEX.LIST and - // META-INF/DEPENDENCIES, which collide when packaging the APK. Merge them, as - // the root build does for the library modules. + // The genai/auth and Firebase transitive dependencies each ship a META-INF/INDEX.LIST and + // META-INF/DEPENDENCIES, which collide when packaging the APK. Merge them, as the root build does + // for the library modules. packaging { resources { merges += "**/META-INF/INDEX.LIST" @@ -48,9 +76,50 @@ android { } } +// Build-time diagnostic for the Firebase example: if neither a google-services.json nor a complete +// set of FIREBASE_* values is present, the produced APK has no usable Firebase config, so the +// Firebase example shows a "no configuration" message at runtime. +run { + val hasGoogleServicesJson = file("google-services.json").exists() + val missingKeys = firebaseConfigKeys.filter { name -> + providers + .gradleProperty(name) + .orElse(providers.environmentVariable(name)) + .orNull + .isNullOrBlank() + } + if (!hasGoogleServicesJson && missingKeys.isNotEmpty()) { + val projectPath = project.path + val log = logger + val message = + "examples/android: building without a usable Firebase configuration. The app will build " + + "and launch, but its Firebase example will show a \"No Firebase configuration found\" " + + "message instead of calling Firebase. Add a google-services.json to examples/android/, " + + "or pass -PFIREBASE_API_KEY=... -PFIREBASE_APP_ID=... -PFIREBASE_PROJECT_ID=... " + + "(missing: ${missingKeys.joinToString()}). See examples/android/README.md." + // Fire only when this app's build/install tasks are actually scheduled (not on every Gradle + // configuration). + gradle.taskGraph.addTaskExecutionGraphListener { graph -> + val buildingThisApp = + graph.allTasks.any { task -> + task.path.startsWith("$projectPath:") && + listOf("assemble", "install", "bundle", "package").any { task.name.startsWith(it) } + } + if (buildingThisApp) log.warn("WARNING: $message") + } + } +} + dependencies { implementation(project(":google-adk-kotlin-core")) + implementation(project(":google-adk-kotlin-firebase")) + implementation(platform(libs.google.firebase.platform)) + implementation(libs.google.firebase.ai) implementation(libs.google.mlkit.genai.prompt) implementation(libs.kotlinx.coroutines.core) implementation(libs.androidx.core) + + // Generates the `@Tool` FunctionTools used by the Firebase example + // (WeatherTools.generatedTools()). + ksp(project(":google-adk-kotlin-processor")) } diff --git a/examples/android/src/main/AndroidManifest.xml b/examples/android/src/main/AndroidManifest.xml index c7e86658..c1d54350 100644 --- a/examples/android/src/main/AndroidManifest.xml +++ b/examples/android/src/main/AndroidManifest.xml @@ -18,6 +18,8 @@ package="com.google.adk.kt.examples.android"> + + + + + + + + diff --git a/examples/android/src/main/kotlin/com/google/adk/kt/examples/android/common/EventText.kt b/examples/android/src/main/kotlin/com/google/adk/kt/examples/android/common/EventText.kt index 63ecea30..50178288 100644 --- a/examples/android/src/main/kotlin/com/google/adk/kt/examples/android/common/EventText.kt +++ b/examples/android/src/main/kotlin/com/google/adk/kt/examples/android/common/EventText.kt @@ -19,7 +19,14 @@ package com.google.adk.kt.examples.android.common import com.google.adk.kt.events.Event /** - * Concatenates the text of every part of this event's content, in order, into a single string - * (empty when the event has no content or no text parts). + * Concatenates this event's visible response text: the text of every non-thought part of its + * content, in order, joined with no separator (empty when there is no such text). + * + * This mirrors how core ADK reconstructs a response's text from its parts (see + * `LlmAgent.maybeSaveOutputToState`): thought parts are excluded, and parts are joined directly + * (not space-separated), because they are fragments of one continuous message — a separator would + * inject spurious spaces, including mid-word across streaming chunks. Core keeps this logic + * private, so the examples re-expose it here as a small shared helper. */ -fun Event.foldTextParts(): String = content?.parts.orEmpty().mapNotNull { it.text }.joinToString("") +fun Event.foldTextParts(): String = + content?.parts.orEmpty().filter { it.thought != true }.mapNotNull { it.text }.joinToString("") diff --git a/examples/android/src/main/kotlin/com/google/adk/kt/examples/android/firebase/FirebaseAppResolver.kt b/examples/android/src/main/kotlin/com/google/adk/kt/examples/android/firebase/FirebaseAppResolver.kt new file mode 100644 index 00000000..d2a939c1 --- /dev/null +++ b/examples/android/src/main/kotlin/com/google/adk/kt/examples/android/firebase/FirebaseAppResolver.kt @@ -0,0 +1,100 @@ +/* + * 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.examples.android.firebase + +import android.content.Context +import android.content.pm.PackageManager +import com.google.firebase.FirebaseApp +import com.google.firebase.FirebaseOptions + +/** + * Resolves the [FirebaseApp] this sample talks to. This is Firebase setup plumbing, kept out of + * [FirebaseChatActivity] so the activity shows only how ADK is used (build an agent, run it). + * + * Resolution order: + * 1. Standard setup: a `google-services.json` in the app module's root, processed by the + * `com.google.gms.google-services` Gradle plugin, auto-initializes the default [FirebaseApp] at + * process start. In a normal Firebase app this is all you need — [resolve] just returns it and + * the rest of this class is irrelevant. + * 2. Fallback: the `FIREBASE_API_KEY` / `FIREBASE_APP_ID` / `FIREBASE_PROJECT_ID` values baked into + * the APK manifest at build time (see `build.gradle.kts` / `AndroidManifest.xml`), used to build + * a named [FirebaseApp] on demand. + * + * Returns null when neither is available; see README.md for setup. + */ +internal object FirebaseAppResolver { + + /** Prefix of the manifest metadata keys holding the build-time fallback config. */ + private const val META_DATA_PREFIX = "com.google.adk." + + /** Non-default FirebaseApp name used for the build-time-config fallback path. */ + private const val FALLBACK_APP_NAME = "adk-firebase-example" + + /** + * Resolves a usable [FirebaseApp], or null if neither the standard nor fallback config exists. + */ + fun resolve(context: Context): FirebaseApp? { + // 1) Standard path: google-services.json + the google-services plugin auto-initialize the + // default FirebaseApp at process start (via Firebase's init ContentProvider). + runCatching { FirebaseApp.getInstance() } + .getOrNull() + ?.let { + return it + } + + // 2) Fallback path: reuse the named app if an earlier call already built it, otherwise assemble + // one from the FIREBASE_* manifest meta-data baked in at build time. + runCatching { FirebaseApp.getInstance(FALLBACK_APP_NAME) } + .getOrNull() + ?.let { + return it + } + val apiKey = bakedMetaData(context, "FIREBASE_API_KEY") ?: return null + val appId = bakedMetaData(context, "FIREBASE_APP_ID") ?: return null + val projectId = bakedMetaData(context, "FIREBASE_PROJECT_ID") ?: return null + + return FirebaseApp.initializeApp( + context.applicationContext, + FirebaseOptions.Builder() + .setApiKey(apiKey) + .setApplicationId(appId) + .setProjectId(projectId) + .build(), + FALLBACK_APP_NAME, + ) + } + + /** + * Reads the `${[META_DATA_PREFIX]}${[token]}` application manifest metadata entry. Returns null + * if it is missing, blank, or still holds its unresolved `${[token]}` build placeholder (i.e. no + * value was supplied at build time). + */ + private fun bakedMetaData(context: Context, token: String): String? { + val raw = + try { + val appInfo = + context.packageManager.getApplicationInfo( + context.packageName, + PackageManager.GET_META_DATA, + ) + appInfo.metaData?.getString(META_DATA_PREFIX + token) + } catch (_: PackageManager.NameNotFoundException) { + null + } + return raw?.takeIf { it.isNotBlank() && !it.contains(token) } + } +} diff --git a/examples/android/src/main/kotlin/com/google/adk/kt/examples/android/firebase/FirebaseChatActivity.kt b/examples/android/src/main/kotlin/com/google/adk/kt/examples/android/firebase/FirebaseChatActivity.kt new file mode 100644 index 00000000..f7772085 --- /dev/null +++ b/examples/android/src/main/kotlin/com/google/adk/kt/examples/android/firebase/FirebaseChatActivity.kt @@ -0,0 +1,167 @@ +/* + * 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.examples.android.firebase + +import android.os.Bundle +import android.view.Gravity +import android.widget.Button +import android.widget.EditText +import android.widget.LinearLayout +import android.widget.ScrollView +import android.widget.TextView +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.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 kotlinx.coroutines.launch + +/** + * Minimal Android example: an [com.google.adk.kt.agents.LlmAgent] backed by the Firebase AI + * (Gemini) model from the `:google-adk-kotlin-firebase` module. The chat UI exercises both plain + * conversation and tool calling (via [WeatherTools]). + * + * The Firebase-setup plumbing lives in [FirebaseAppResolver]; the agent wiring lives in + * [FirebaseChatAgent]. What remains here is the typical ADK usage: build an [InMemoryRunner] around + * an agent and drive it with [InMemoryRunner.runAsync]. + * + * Unlike the on-device examples, this one talks to the cloud Firebase AI (Gemini) backend, so it + * needs a Firebase configuration and network access; see the app README.md for setup. + */ +// Hardcoded UI strings are intentional in this minimal example; a real app would use resources. +@Suppress("SetTextI18n") +class FirebaseChatActivity : ScopedExampleActivity() { + + private val sessionService = InMemorySessionService() + + /** + * Built in [onCreate] once a FirebaseApp is resolved; null means the agent could not be created. + */ + private var runner: InMemoryRunner? = null + + private lateinit var transcript: TextView + private lateinit var input: EditText + private lateinit var sendButton: Button + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setExampleContentView("Firebase AI chat", buildContent()) + + val firebaseApp = FirebaseAppResolver.resolve(applicationContext) + if (firebaseApp == null) { + appendToTranscript( + "No Firebase configuration found. Add a google-services.json to the examples/android/ " + + "module (standard setup), or rebuild supplying -PFIREBASE_API_KEY=... " + + "-PFIREBASE_APP_ID=... -PFIREBASE_PROJECT_ID=... (or the matching environment " + + "variables). See the app README.md." + ) + sendButton.isEnabled = false + return + } + + runner = + try { + InMemoryRunner( + agent = FirebaseChatAgent.create(firebaseApp), + appName = APP_NAME, + sessionService = sessionService, + ) + } catch (e: Throwable) { + appendToTranscript("Failed to build agent: ${e.message ?: e::class.simpleName}") + sendButton.isEnabled = false + return + } + + appendToTranscript( + "Ready. Try: \"Tell me about the planet Earth\" or " + + "\"What is the current temperature in Mountain View?\"" + ) + } + + private fun sendToAgent(text: String) { + val activeRunner = runner ?: return + appendToTranscript("you: $text") + scope.launch { + try { + activeRunner + .runAsync( + userId = USER_ID, + sessionId = SESSION_ID, + newMessage = Content(role = Role.USER, parts = listOf(Part(text = text))), + ) + .collect { event -> + val reply = event.foldTextParts() + if (event.author == FirebaseChatAgent.NAME && reply.isNotBlank()) { + appendToTranscript("${FirebaseChatAgent.NAME}: $reply") + } + } + } catch (e: Exception) { + appendToTranscript("Error: ${e.message ?: e::class.simpleName}") + } + } + } + + private fun appendToTranscript(line: String) { + runOnUiThread { transcript.append("$line\n\n") } + } + + private fun buildContent(): LinearLayout { + transcript = TextView(this).apply { setPadding(24, 24, 24, 24) } + val scroll = + ScrollView(this).apply { + layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f) + addView(transcript) + } + input = + EditText(this).apply { + hint = "Type a message…" + layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f) + } + sendButton = + Button(this).apply { + text = "Send" + setOnClickListener { + val text = input.text.toString() + if (text.isNotBlank()) { + input.text.clear() + sendToAgent(text) + } + } + } + val inputRow = + LinearLayout(this).apply { + orientation = LinearLayout.HORIZONTAL + gravity = Gravity.CENTER_VERTICAL + addView(input) + addView(sendButton) + } + return LinearLayout(this).apply { + orientation = LinearLayout.VERTICAL + addView(scroll) + addView(inputRow) + } + } + + private companion object { + const val APP_NAME = "FirebaseChatExample" + const val USER_ID = "local-user" + const val SESSION_ID = "local-session" + } +} diff --git a/examples/android/src/main/kotlin/com/google/adk/kt/examples/android/firebase/FirebaseChatAgent.kt b/examples/android/src/main/kotlin/com/google/adk/kt/examples/android/firebase/FirebaseChatAgent.kt new file mode 100644 index 00000000..8ecfb950 --- /dev/null +++ b/examples/android/src/main/kotlin/com/google/adk/kt/examples/android/firebase/FirebaseChatAgent.kt @@ -0,0 +1,57 @@ +/* + * 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.examples.android.firebase + +import com.google.adk.firebase.models.Firebase +import com.google.adk.kt.agents.Instruction +import com.google.adk.kt.agents.LlmAgent +import com.google.firebase.FirebaseApp +import com.google.firebase.ai.FirebaseAI + +/** + * Builds the [LlmAgent] used by [FirebaseChatActivity], backed by the Firebase AI (Gemini) model + * from the `:google-adk-kotlin-firebase` module. + * + * The agent both answers general questions (plain chat) and can call [WeatherTools] to look up a + * temperature, mirroring the two scenarios proven by the module's `FirebaseIntegrationTest`. + */ +internal object FirebaseChatAgent { + const val NAME: String = "firebase_agent" + + /** + * The Firebase AI model to use. Any model available to your Firebase project works; override it + * if this one is not enabled for you. + */ + private const val MODEL_NAME: String = "gemini-3.5-flash" + + /** Builds the agent against the given (already initialized) [firebaseApp]. */ + fun create(firebaseApp: FirebaseApp): LlmAgent = + LlmAgent( + name = NAME, + model = Firebase.create(MODEL_NAME, FirebaseAI.getInstance(firebaseApp)), + instruction = + Instruction( + """ + You are a helpful assistant. Answer general questions in one or two sentences. When the + user asks about the current temperature somewhere, call the get_current_temperature tool + and state the exact value it returns. + """ + .trimIndent() + ), + tools = WeatherTools().generatedTools(), + ) +} diff --git a/examples/android/src/main/kotlin/com/google/adk/kt/examples/android/firebase/WeatherTools.kt b/examples/android/src/main/kotlin/com/google/adk/kt/examples/android/firebase/WeatherTools.kt new file mode 100644 index 00000000..4423f427 --- /dev/null +++ b/examples/android/src/main/kotlin/com/google/adk/kt/examples/android/firebase/WeatherTools.kt @@ -0,0 +1,39 @@ +/* + * 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.examples.android.firebase + +import com.google.adk.kt.annotations.Param +import com.google.adk.kt.annotations.Tool + +/** + * A trivial tool exposed to the Firebase-backed agent through the KSP `@Tool` processor. Calling + * [com.google.adk.kt.examples.android.firebase.WeatherTools.generatedTools] (generated by the + * processor) turns this into a `FunctionTool` the model can invoke. + * + * It always returns the same made-up temperature: a value the model cannot know on its own, which + * makes it obvious in the transcript that the tool was actually called rather than the answer being + * hallucinated. + */ +class WeatherTools { + @Tool( + name = "get_current_temperature", + description = "Returns the current temperature in Celsius for a given location.", + ) + fun getCurrentTemperature( + @Param("The city to look up, e.g. 'Mountain View'.") location: String + ): Map = mapOf("temperature_celsius" to 42) +} diff --git a/examples/android/src/main/kotlin/com/google/adk/kt/examples/android/home/HomeActivity.kt b/examples/android/src/main/kotlin/com/google/adk/kt/examples/android/home/HomeActivity.kt index af600e03..f134db4d 100644 --- a/examples/android/src/main/kotlin/com/google/adk/kt/examples/android/home/HomeActivity.kt +++ b/examples/android/src/main/kotlin/com/google/adk/kt/examples/android/home/HomeActivity.kt @@ -22,6 +22,7 @@ import android.os.Bundle import android.widget.Button import android.widget.LinearLayout import com.google.adk.kt.examples.android.common.setExampleContentView +import com.google.adk.kt.examples.android.firebase.FirebaseChatActivity import com.google.adk.kt.examples.android.mlkitchat.MlKitChatActivity import com.google.adk.kt.examples.android.roomsession.RoomSessionActivity import com.google.adk.kt.examples.android.skillsassetsource.SkillsAssetSourceActivity @@ -65,6 +66,12 @@ class HomeActivity : Activity() { MlKitChatActivity::class.java, ) ) + addView( + exampleButton( + "Firebase AI — cloud Gemini (Firebase AI Logic), plain chat + tool calling", + FirebaseChatActivity::class.java, + ) + ) } private fun exampleButton(label: String, activity: Class): Button = diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cba24b5c..1da09305 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,6 +20,7 @@ google-genai-kotlin = "0.2.0" google-gson = "2.10.1" google-mlkit-genai = "1.0.0-beta2" google-protobuf-javalite = "3.25.5" +google-services = "4.4.4" google-truth = "1.4.5" gradle-test-retry = "1.6.5" graphviz = "0.18.1" @@ -114,6 +115,7 @@ snakeyaml = { module = "org.yaml:snakeyaml", version.ref = "snakeyaml" } [plugins] # go/keep-sorted start dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } +google-services = { id = "com.google.gms.google-services", version.ref = "google-services" } gradle-test-retry = { id = "org.gradle.test-retry", version.ref = "gradle-test-retry"} ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } # go/keep-sorted end