Skip to content

Commit 2532225

Browse files
google-genai-botcopybara-github
authored andcommitted
feat: Adds support for YAML-based basic agents
This commit adds support for loading agents from YAML files. Agents can now be defined with a YAML configuration file, which is more human-friendly than code. PiperOrigin-RevId: 790945229
1 parent bd0bb57 commit 2532225

File tree

11 files changed

+954
-10
lines changed

11 files changed

+954
-10
lines changed

core/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@
111111
<artifactId>jackson-datatype-jsr310</artifactId>
112112
<version>${jackson.version}</version>
113113
</dependency>
114+
<dependency>
115+
<groupId>com.fasterxml.jackson.dataformat</groupId>
116+
<artifactId>jackson-dataformat-yaml</artifactId>
117+
<version>${jackson.version}</version>
118+
</dependency>
114119
<dependency>
115120
<groupId>com.google.protobuf</groupId>
116121
<artifactId>protobuf-java</artifactId>

core/src/main/java/com/google/adk/agents/BaseAgent.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.google.adk.agents.Callbacks.BeforeAgentCallback;
2424
import com.google.adk.events.Event;
2525
import com.google.common.collect.ImmutableList;
26+
import com.google.errorprone.annotations.DoNotCall;
2627
import com.google.genai.types.Content;
2728
import io.opentelemetry.api.trace.Span;
2829
import io.opentelemetry.api.trace.Tracer;
@@ -359,4 +360,19 @@ public Flowable<Event> runLive(InvocationContext parentContext) {
359360
* @return stream of agent-generated events.
360361
*/
361362
protected abstract Flowable<Event> runLiveImpl(InvocationContext invocationContext);
363+
364+
/**
365+
* Creates a new agent instance from a configuration object.
366+
*
367+
* @param config Agent configuration.
368+
* @param configAbsPath Absolute path to the configuration file.
369+
* @return new agent instance.
370+
*/
371+
// TODO: Makes `BaseAgent.fromConfig` a final method and let sub-class to optionally override
372+
// `_parse_config` to update kwargs if needed.
373+
@DoNotCall("Always throws java.lang.UnsupportedOperationException")
374+
public static BaseAgent fromConfig(BaseAgentConfig config, String configAbsPath) {
375+
throw new UnsupportedOperationException(
376+
"BaseAgent is abstract. Override fromConfig in concrete subclasses.");
377+
}
362378
}

core/src/main/java/com/google/adk/agents/BaseAgentConfig.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,14 @@
2121
/**
2222
* Base configuration for all agents.
2323
*
24-
* <p>workInProgress: Config agent features are not yet ready for public use.
24+
* <p>TODO: Config agent features are not yet ready for public use.
2525
*/
2626
public class BaseAgentConfig {
2727
private String name;
2828
private String description = "";
29+
// TODO: Add agentClassType enum to the config and handle different values from user
30+
// input.e.g.LLM_AGENT, LlmAgent
31+
private String agentClass = "BaseAgent";
2932

3033
@JsonProperty(value = "name", required = true)
3134
public String name() {
@@ -44,4 +47,13 @@ public String description() {
4447
public void setDescription(String description) {
4548
this.description = description;
4649
}
50+
51+
@JsonProperty("agent_class")
52+
public String agentClass() {
53+
return agentClass;
54+
}
55+
56+
public void setAgentClass(String agentClass) {
57+
this.agentClass = agentClass;
58+
}
4759
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.adk.agents;
18+
19+
import static com.google.common.base.Strings.isNullOrEmpty;
20+
21+
import com.fasterxml.jackson.databind.DeserializationFeature;
22+
import com.fasterxml.jackson.databind.ObjectMapper;
23+
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
24+
import java.io.File;
25+
import java.io.FileInputStream;
26+
import java.io.IOException;
27+
import java.io.InputStream;
28+
import org.slf4j.Logger;
29+
import org.slf4j.LoggerFactory;
30+
31+
/**
32+
* Utility class for loading agent configurations from YAML files.
33+
*
34+
* <p>TODO: Config agent features are not yet ready for public use.
35+
*/
36+
public final class ConfigAgentUtils {
37+
38+
private static final Logger logger = LoggerFactory.getLogger(ConfigAgentUtils.class);
39+
40+
private ConfigAgentUtils() {}
41+
42+
/**
43+
* Load agent from a YAML config file path.
44+
*
45+
* @param configPath the path to a YAML config file
46+
* @return the created agent instance as a {@link BaseAgent}
47+
* @throws ConfigurationException if loading fails
48+
*/
49+
public static BaseAgent fromConfig(String configPath) throws ConfigurationException {
50+
51+
File configFile = new File(configPath);
52+
if (!configFile.exists()) {
53+
logger.error("Config file not found: {}", configPath);
54+
throw new ConfigurationException("Config file not found: " + configPath);
55+
}
56+
57+
String absolutePath = configFile.getAbsolutePath();
58+
59+
try {
60+
// Load the base config to determine the agent class
61+
BaseAgentConfig baseConfig = loadConfigAsType(absolutePath, BaseAgentConfig.class);
62+
Class<? extends BaseAgent> agentClass = resolveAgentClass(baseConfig.agentClass());
63+
64+
// Load the config file with the specific config class
65+
Class<? extends BaseAgentConfig> configClass = getConfigClassForAgent(agentClass);
66+
BaseAgentConfig config = loadConfigAsType(absolutePath, configClass);
67+
logger.info("agentClass value = '{}'", config.agentClass());
68+
69+
// Use reflection to call the fromConfig method with the correct types
70+
java.lang.reflect.Method fromConfigMethod =
71+
agentClass.getDeclaredMethod("fromConfig", configClass, String.class);
72+
return (BaseAgent) fromConfigMethod.invoke(null, config, absolutePath);
73+
74+
} catch (ConfigurationException e) {
75+
throw e;
76+
} catch (Exception e) {
77+
throw new ConfigurationException("Failed to create agent from config: " + configPath, e);
78+
}
79+
}
80+
81+
/**
82+
* Load configuration from a YAML file path as a specific type.
83+
*
84+
* @param configPath the absolute path to the config file
85+
* @param configClass the class to deserialize the config into
86+
* @return the loaded configuration
87+
* @throws ConfigurationException if loading fails
88+
*/
89+
private static <T extends BaseAgentConfig> T loadConfigAsType(
90+
String configPath, Class<T> configClass) throws ConfigurationException {
91+
try (InputStream inputStream = new FileInputStream(configPath)) {
92+
ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
93+
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
94+
return mapper.readValue(inputStream, configClass);
95+
} catch (IOException e) {
96+
throw new ConfigurationException("Failed to load or parse config file: " + configPath, e);
97+
}
98+
}
99+
100+
/**
101+
* Resolves the agent class based on the agent class name from the configuration.
102+
*
103+
* @param agentClassName the name of the agent class from the config
104+
* @return the corresponding agent class
105+
* @throws ConfigurationException if the agent class is not supported
106+
*/
107+
private static Class<? extends BaseAgent> resolveAgentClass(String agentClassName)
108+
throws ConfigurationException {
109+
// If no agent_class is specified in the yaml file, it will default to LlmAgent.
110+
if (isNullOrEmpty(agentClassName)
111+
|| agentClassName.equals("LlmAgent")
112+
|| agentClassName.equals("BaseAgent")) {
113+
return LlmAgent.class;
114+
}
115+
116+
// TODO: Support more agent classes
117+
// Example for future extensions:
118+
// if (agentClassName.equals("CustomAgent")) {
119+
// return CustomAgent.class;
120+
// }
121+
122+
throw new ConfigurationException("agentClass '" + agentClassName + "' is not supported yet");
123+
}
124+
125+
/**
126+
* Maps agent classes to their corresponding config classes.
127+
*
128+
* @param agentClass the agent class
129+
* @return the corresponding config class
130+
*/
131+
private static Class<? extends BaseAgentConfig> getConfigClassForAgent(
132+
Class<? extends BaseAgent> agentClass) {
133+
134+
if (agentClass == LlmAgent.class) {
135+
return LlmAgentConfig.class;
136+
}
137+
138+
// TODO: Add more agent class to config class mappings as needed
139+
// Example:
140+
// if (agentClass == CustomAgent.class) {
141+
// return CustomAgentConfig.class;
142+
// }
143+
144+
// Default fallback to BaseAgentConfig
145+
return BaseAgentConfig.class;
146+
}
147+
148+
/** Exception thrown when configuration is invalid. */
149+
public static class ConfigurationException extends Exception {
150+
public ConfigurationException(String message) {
151+
super(message);
152+
}
153+
154+
public ConfigurationException(String message, Throwable cause) {
155+
super(message, cause);
156+
}
157+
}
158+
}

core/src/main/java/com/google/adk/agents/LlmAgent.java

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package com.google.adk.agents;
1818

19+
import static com.google.common.base.Strings.isNullOrEmpty;
20+
import static com.google.common.base.Strings.nullToEmpty;
1921
import static java.util.stream.Collectors.joining;
2022

2123
import com.fasterxml.jackson.core.JsonProcessingException;
@@ -45,6 +47,7 @@
4547
import com.google.adk.flows.llmflows.BaseLlmFlow;
4648
import com.google.adk.flows.llmflows.SingleFlow;
4749
import com.google.adk.models.BaseLlm;
50+
import com.google.adk.models.Gemini;
4851
import com.google.adk.models.Model;
4952
import com.google.adk.tools.BaseTool;
5053
import com.google.adk.tools.BaseToolset;
@@ -845,4 +848,95 @@ private Model resolveModelInternal() {
845848
}
846849
throw new IllegalStateException("No model found for agent " + name() + " or its ancestors.");
847850
}
851+
852+
/**
853+
* Creates an LlmAgent from configuration.
854+
*
855+
* @param config the agent configuration
856+
* @param configAbsPath The absolute path to the agent config file. This is needed for resolving
857+
* relative paths for e.g. tools.
858+
* @return the configured LlmAgent
859+
* @throws ConfigAgentUtils.ConfigurationException if the configuration is invalid
860+
* <p>TODO: Config agent features are not yet ready for public use.
861+
*/
862+
public static LlmAgent fromConfig(LlmAgentConfig config, String configAbsPath)
863+
throws ConfigAgentUtils.ConfigurationException {
864+
logger.debug("Creating LlmAgent from config: {}", config.name());
865+
866+
// Validate required fields
867+
if (config.name() == null || config.name().trim().isEmpty()) {
868+
throw new ConfigAgentUtils.ConfigurationException("Agent name is required");
869+
}
870+
871+
if (config.instruction() == null || config.instruction().trim().isEmpty()) {
872+
throw new ConfigAgentUtils.ConfigurationException("Agent instruction is required");
873+
}
874+
875+
// Create builder with required fields
876+
Builder builder =
877+
LlmAgent.builder()
878+
.name(config.name())
879+
.description(nullToEmpty(config.description()))
880+
.instruction(config.instruction());
881+
882+
// Set optional model configuration
883+
if (config.model() != null && !config.model().trim().isEmpty()) {
884+
logger.info("Configuring model: {}", config.model());
885+
886+
// TODO: resolve model name
887+
if (config.model().startsWith("gemini")) {
888+
try {
889+
// Check for API key in system properties (for testing) or environment variables
890+
String apiKey = System.getProperty("GOOGLE_API_KEY");
891+
if (isNullOrEmpty(apiKey)) {
892+
apiKey = System.getProperty("GEMINI_API_KEY");
893+
}
894+
if (isNullOrEmpty(apiKey)) {
895+
apiKey = System.getenv("GOOGLE_API_KEY");
896+
}
897+
if (isNullOrEmpty(apiKey)) {
898+
apiKey = System.getenv("GEMINI_API_KEY");
899+
}
900+
901+
Gemini.Builder geminiBuilder = Gemini.builder().modelName(config.model());
902+
if (apiKey != null && !apiKey.isEmpty()) {
903+
geminiBuilder.apiKey(apiKey);
904+
}
905+
906+
BaseLlm model = geminiBuilder.build();
907+
builder.model(model);
908+
logger.debug("Successfully configured Gemini model: {}", config.model());
909+
} catch (RuntimeException e) {
910+
logger.warn(
911+
"Failed to create Gemini model '{}'. The agent will use the default LLM. Error: {}",
912+
config.model(),
913+
e.getMessage());
914+
}
915+
} else {
916+
logger.warn(
917+
"Model '{}' is not a supported Gemini model. The agent will use the default LLM.",
918+
config.model());
919+
}
920+
}
921+
922+
// Set optional transfer configuration
923+
if (config.disallowTransferToParent() != null) {
924+
builder.disallowTransferToParent(config.disallowTransferToParent());
925+
}
926+
927+
if (config.disallowTransferToPeers() != null) {
928+
builder.disallowTransferToPeers(config.disallowTransferToPeers());
929+
}
930+
931+
// Set optional output key
932+
if (config.outputKey() != null && !config.outputKey().trim().isEmpty()) {
933+
builder.outputKey(config.outputKey());
934+
}
935+
936+
// Build and return the agent
937+
LlmAgent agent = builder.build();
938+
logger.info("Successfully created LlmAgent: {}", agent.name());
939+
940+
return agent;
941+
}
848942
}

core/src/main/java/com/google/adk/agents/LlmAgentConfig.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
/**
2222
* Configuration for LlmAgent.
2323
*
24-
* <p>workInProgress: Config agent features are not yet ready for public use.
24+
* <p>TODO: Config agent features are not yet ready for public use.
2525
*/
2626
public class LlmAgentConfig extends BaseAgentConfig {
2727
private String model;
@@ -30,6 +30,11 @@ public class LlmAgentConfig extends BaseAgentConfig {
3030
private Boolean disallowTransferToPeers;
3131
private String outputKey;
3232

33+
public LlmAgentConfig() {
34+
super();
35+
setAgentClass("LlmAgent");
36+
}
37+
3338
// Non-standard accessors with JsonProperty annotations
3439
@JsonProperty("model")
3540
public String model() {

0 commit comments

Comments
 (0)