From 2a813396e6ee422aa2992c7c7a157853c7e898b8 Mon Sep 17 00:00:00 2001 From: YuqiGuo105 Date: Mon, 11 May 2026 22:07:48 -0600 Subject: [PATCH] feat: add AdkConfiguration abstraction layer for programmatic config injection Replace hardcoded System.getenv() calls with a three-tier fallback chain: 1. Programmatic overrides via AdkConfiguration.set() (highest priority) 2. JVM system properties via System.getProperty() 3. OS environment variables via System.getenv() (backward compatible) Files changed: - core/.../config/AdkConfiguration.java (new): central config provider with ConcurrentHashMap-backed programmatic overrides, set/get/clear/ clearAll API, and Optional-returning get() with full fallback chain. - core/.../sessions/ApiClient.java: replace 3x getenv with AdkConfiguration (GOOGLE_API_KEY, GOOGLE_CLOUD_PROJECT, GOOGLE_CLOUD_LOCATION) - core/.../tools/retrieval/VertexAiRagRetrieval.java: replace getenv with AdkConfiguration (GOOGLE_GENAI_USE_VERTEXAI) - core/.../models/ApigeeLlm.java: replace 2x getenv with AdkConfiguration (APIGEE_PROXY_URL, isEnvEnabled helper) - core/.../config/AdkConfigurationTest.java (new): 8 unit tests covering fallback priority, clear/clearAll, null safety, and defaults. Fixes #1022 --- .../google/adk/config/AdkConfiguration.java | 104 ++++++++++++++++++ .../java/com/google/adk/models/ApigeeLlm.java | 5 +- .../com/google/adk/sessions/ApiClient.java | 9 +- .../tools/retrieval/VertexAiRagRetrieval.java | 4 +- .../adk/config/AdkConfigurationTest.java | 88 +++++++++++++++ 5 files changed, 204 insertions(+), 6 deletions(-) create mode 100644 core/src/main/java/com/google/adk/config/AdkConfiguration.java create mode 100644 core/src/test/java/com/google/adk/config/AdkConfigurationTest.java diff --git a/core/src/main/java/com/google/adk/config/AdkConfiguration.java b/core/src/main/java/com/google/adk/config/AdkConfiguration.java new file mode 100644 index 000000000..946fb30e2 --- /dev/null +++ b/core/src/main/java/com/google/adk/config/AdkConfiguration.java @@ -0,0 +1,104 @@ +/* + * Copyright 2025 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.config; + +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * Central configuration provider for the ADK SDK. + * + *

Resolves configuration values using the following fallback chain (highest priority first): + * + *

    + *
  1. Programmatic values set via {@link #set(String, String)}. + *
  2. JVM system properties ({@link System#getProperty(String)}). + *
  3. Operating system environment variables ({@link System#getenv(String)}) — preserved for + * backward compatibility. + *
+ * + *

This abstraction allows applications (e.g. Spring Boot) to inject configuration that + * originates from {@code application.yaml}, secret managers, or any other source, without relying + * on the immutable OS environment. + */ +public final class AdkConfiguration { + + private static final ConcurrentMap OVERRIDES = new ConcurrentHashMap<>(); + + private AdkConfiguration() {} + + /** + * Programmatically sets a configuration value. Takes precedence over system properties and + * environment variables. + * + * @param key the configuration key (typically the same name as the legacy environment variable) + * @param value the value to associate with the key; must not be {@code null}. Use {@link + * #clear(String)} to remove an entry. + */ + public static void set(String key, String value) { + if (key == null) { + throw new IllegalArgumentException("key must not be null"); + } + if (value == null) { + throw new IllegalArgumentException("value must not be null; use clear(key) to remove"); + } + OVERRIDES.put(key, value); + } + + /** Removes a programmatic override for the given key, if any. */ + public static void clear(String key) { + if (key != null) { + OVERRIDES.remove(key); + } + } + + /** Removes all programmatic overrides. Primarily intended for tests. */ + public static void clearAll() { + OVERRIDES.clear(); + } + + /** + * Resolves a configuration value using the fallback chain described in the class javadoc. + * + * @param key the configuration key + * @return an {@link Optional} containing the resolved value, or empty if no source provides one + */ + public static Optional get(String key) { + if (key == null) { + return Optional.empty(); + } + String override = OVERRIDES.get(key); + if (override != null) { + return Optional.of(override); + } + String systemProperty = System.getProperty(key); + if (systemProperty != null) { + return Optional.of(systemProperty); + } + return Optional.ofNullable(System.getenv(key)); + } + + /** + * Resolves a configuration value, returning the provided default if no source provides one. + * + * @param key the configuration key + * @param defaultValue value to return when the key cannot be resolved + */ + public static String getOrDefault(String key, String defaultValue) { + return get(key).orElse(defaultValue); + } +} diff --git a/core/src/main/java/com/google/adk/models/ApigeeLlm.java b/core/src/main/java/com/google/adk/models/ApigeeLlm.java index 088e0af76..db2205322 100644 --- a/core/src/main/java/com/google/adk/models/ApigeeLlm.java +++ b/core/src/main/java/com/google/adk/models/ApigeeLlm.java @@ -19,6 +19,7 @@ import static com.google.common.base.Strings.isNullOrEmpty; import com.google.adk.Version; +import com.google.adk.config.AdkConfiguration; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; import com.google.errorprone.annotations.CanIgnoreReturnValue; @@ -73,7 +74,7 @@ private ApigeeLlm(String modelName, String proxyUrl, Map customH String effectiveProxyUrl = proxyUrl; if (isNullOrEmpty(effectiveProxyUrl)) { - effectiveProxyUrl = System.getenv(APIGEE_PROXY_URL_ENV_VARIABLE_NAME); + effectiveProxyUrl = AdkConfiguration.get(APIGEE_PROXY_URL_ENV_VARIABLE_NAME).orElse(null); } if (isNullOrEmpty(effectiveProxyUrl)) { throw new IllegalArgumentException( @@ -306,7 +307,7 @@ private static boolean validateModelString(String model) { } private static boolean isEnvEnabled(String envVarName) { - String value = System.getenv(envVarName); + String value = AdkConfiguration.get(envVarName).orElse(null); return Boolean.parseBoolean(value) || Objects.equals(value, "1"); } diff --git a/core/src/main/java/com/google/adk/sessions/ApiClient.java b/core/src/main/java/com/google/adk/sessions/ApiClient.java index 1b0485dd2..75ad98678 100644 --- a/core/src/main/java/com/google/adk/sessions/ApiClient.java +++ b/core/src/main/java/com/google/adk/sessions/ApiClient.java @@ -18,6 +18,7 @@ import static com.google.common.base.StandardSystemProperty.JAVA_VERSION; +import com.google.adk.config.AdkConfiguration; import com.google.auth.oauth2.GoogleCredentials; import com.google.common.base.Ascii; import com.google.common.base.Strings; @@ -46,7 +47,7 @@ abstract class ApiClient { /** Constructs an ApiClient for Google AI APIs. */ ApiClient(@Nullable String apiKey, @Nullable HttpOptions customHttpOptions) { - this.apiKey = apiKey != null ? apiKey : System.getenv("GOOGLE_API_KEY"); + this.apiKey = apiKey != null ? apiKey : AdkConfiguration.get("GOOGLE_API_KEY").orElse(null); if (Strings.isNullOrEmpty(this.apiKey)) { throw new IllegalArgumentException( @@ -74,7 +75,8 @@ abstract class ApiClient { @Nullable GoogleCredentials credentials, @Nullable HttpOptions customHttpOptions) { - this.project = project != null ? project : System.getenv("GOOGLE_CLOUD_PROJECT"); + this.project = + project != null ? project : AdkConfiguration.get("GOOGLE_CLOUD_PROJECT").orElse(null); if (Strings.isNullOrEmpty(this.project)) { throw new IllegalArgumentException( @@ -82,7 +84,8 @@ abstract class ApiClient { + " GOOGLE_CLOUD_PROJECT."); } - this.location = location != null ? location : System.getenv("GOOGLE_CLOUD_LOCATION"); + this.location = + location != null ? location : AdkConfiguration.get("GOOGLE_CLOUD_LOCATION").orElse(null); if (Strings.isNullOrEmpty(this.location)) { throw new IllegalArgumentException( diff --git a/core/src/main/java/com/google/adk/tools/retrieval/VertexAiRagRetrieval.java b/core/src/main/java/com/google/adk/tools/retrieval/VertexAiRagRetrieval.java index a2720aae5..0ac28cc20 100644 --- a/core/src/main/java/com/google/adk/tools/retrieval/VertexAiRagRetrieval.java +++ b/core/src/main/java/com/google/adk/tools/retrieval/VertexAiRagRetrieval.java @@ -18,6 +18,7 @@ import static com.google.common.collect.ImmutableList.toImmutableList; +import com.google.adk.config.AdkConfiguration; import com.google.adk.models.LlmRequest; import com.google.adk.tools.ToolContext; import com.google.adk.utils.ModelNameUtils; @@ -106,7 +107,8 @@ public Completable processLlmRequest( LlmRequest.Builder llmRequestBuilder, ToolContext toolContext) { LlmRequest llmRequest = llmRequestBuilder.build(); // Use Gemini built-in Vertex AI RAG tool for Gemini models when using Vertex AI API Model - boolean useVertexAi = Boolean.parseBoolean(System.getenv("GOOGLE_GENAI_USE_VERTEXAI")); + boolean useVertexAi = + Boolean.parseBoolean(AdkConfiguration.getOrDefault("GOOGLE_GENAI_USE_VERTEXAI", "false")); if (useVertexAi && llmRequest.model().filter(ModelNameUtils::isGeminiModel).isPresent()) { GenerateContentConfig config = llmRequest.config().orElseGet(() -> GenerateContentConfig.builder().build()); diff --git a/core/src/test/java/com/google/adk/config/AdkConfigurationTest.java b/core/src/test/java/com/google/adk/config/AdkConfigurationTest.java new file mode 100644 index 000000000..fe721b3b7 --- /dev/null +++ b/core/src/test/java/com/google/adk/config/AdkConfigurationTest.java @@ -0,0 +1,88 @@ +/* + * Copyright 2025 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.config; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class AdkConfigurationTest { + + private static final String KEY = "ADK_CONFIG_TEST_KEY"; + + @BeforeEach + void setUp() { + AdkConfiguration.clearAll(); + System.clearProperty(KEY); + } + + @AfterEach + void tearDown() { + AdkConfiguration.clearAll(); + System.clearProperty(KEY); + } + + @Test + void get_returnsEmpty_whenNoSourceProvidesValue() { + assertThat(AdkConfiguration.get(KEY)).isEmpty(); + } + + @Test + void get_returnsSystemProperty_whenSet() { + System.setProperty(KEY, "from-sysprop"); + assertThat(AdkConfiguration.get(KEY)).hasValue("from-sysprop"); + } + + @Test + void set_takesPrecedenceOverSystemProperty() { + System.setProperty(KEY, "from-sysprop"); + AdkConfiguration.set(KEY, "from-programmatic"); + assertThat(AdkConfiguration.get(KEY)).hasValue("from-programmatic"); + } + + @Test + void clear_removesProgrammaticOverride_andFallsBackToSystemProperty() { + System.setProperty(KEY, "from-sysprop"); + AdkConfiguration.set(KEY, "from-programmatic"); + AdkConfiguration.clear(KEY); + assertThat(AdkConfiguration.get(KEY)).hasValue("from-sysprop"); + } + + @Test + void getOrDefault_returnsDefault_whenUnset() { + assertThat(AdkConfiguration.getOrDefault(KEY, "fallback")).isEqualTo("fallback"); + } + + @Test + void getOrDefault_returnsResolvedValue_whenSet() { + AdkConfiguration.set(KEY, "resolved"); + assertThat(AdkConfiguration.getOrDefault(KEY, "fallback")).isEqualTo("resolved"); + } + + @Test + void set_throws_onNullKeyOrValue() { + assertThrows(IllegalArgumentException.class, () -> AdkConfiguration.set(null, "v")); + assertThrows(IllegalArgumentException.class, () -> AdkConfiguration.set(KEY, null)); + } + + @Test + void get_returnsEmpty_forNullKey() { + assertThat(AdkConfiguration.get(null)).isEmpty(); + } +}