Skip to content

Commit f27f48c

Browse files
Doris26copybara-github
authored andcommitted
feat: Implement tool configuration loading
This change introduces the ability to specify built-in tools in the `LlmAgent` YAML configuration. PiperOrigin-RevId: 793716931
1 parent 383297a commit f27f48c

File tree

4 files changed

+256
-4
lines changed

4 files changed

+256
-4
lines changed

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

Lines changed: 128 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import com.google.adk.agents.Callbacks.BeforeToolCallback;
4040
import com.google.adk.agents.Callbacks.BeforeToolCallbackBase;
4141
import com.google.adk.agents.Callbacks.BeforeToolCallbackSync;
42+
import com.google.adk.agents.ConfigAgentUtils.ConfigurationException;
4243
import com.google.adk.events.Event;
4344
import com.google.adk.examples.BaseExampleProvider;
4445
import com.google.adk.examples.Example;
@@ -49,7 +50,9 @@
4950
import com.google.adk.models.LlmRegistry;
5051
import com.google.adk.models.Model;
5152
import com.google.adk.tools.BaseTool;
53+
import com.google.adk.tools.BaseTool.ToolConfig;
5254
import com.google.adk.tools.BaseToolset;
55+
import com.google.common.base.CaseFormat;
5356
import com.google.common.base.Preconditions;
5457
import com.google.common.collect.ImmutableList;
5558
import com.google.errorprone.annotations.CanIgnoreReturnValue;
@@ -59,6 +62,7 @@
5962
import io.reactivex.rxjava3.core.Flowable;
6063
import io.reactivex.rxjava3.core.Maybe;
6164
import io.reactivex.rxjava3.core.Single;
65+
import java.lang.reflect.Constructor;
6266
import java.util.ArrayList;
6367
import java.util.List;
6468
import java.util.Map;
@@ -864,20 +868,20 @@ private Model resolveModelInternal() {
864868
* @param configAbsPath The absolute path to the agent config file. This is needed for resolving
865869
* relative paths for e.g. tools.
866870
* @return the configured LlmAgent
867-
* @throws ConfigAgentUtils.ConfigurationException if the configuration is invalid
871+
* @throws ConfigurationException if the configuration is invalid
868872
* <p>TODO: Config agent features are not yet ready for public use.
869873
*/
870874
public static LlmAgent fromConfig(LlmAgentConfig config, String configAbsPath)
871-
throws ConfigAgentUtils.ConfigurationException {
875+
throws ConfigurationException {
872876
logger.debug("Creating LlmAgent from config: {}", config.name());
873877

874878
// Validate required fields
875879
if (config.name() == null || config.name().trim().isEmpty()) {
876-
throw new ConfigAgentUtils.ConfigurationException("Agent name is required");
880+
throw new ConfigurationException("Agent name is required");
877881
}
878882

879883
if (config.instruction() == null || config.instruction().trim().isEmpty()) {
880-
throw new ConfigAgentUtils.ConfigurationException("Agent instruction is required");
884+
throw new ConfigurationException("Agent instruction is required");
881885
}
882886

883887
// Create builder with required fields
@@ -891,6 +895,14 @@ public static LlmAgent fromConfig(LlmAgentConfig config, String configAbsPath)
891895
builder.model(config.model());
892896
}
893897

898+
try {
899+
if (config.tools() != null) {
900+
builder.tools(resolveTools(config.tools(), configAbsPath));
901+
}
902+
} catch (ConfigurationException e) {
903+
throw new ConfigurationException("Error resolving tools for agent " + config.name(), e);
904+
}
905+
894906
// Set optional transfer configuration
895907
if (config.disallowTransferToParent() != null) {
896908
builder.disallowTransferToParent(config.disallowTransferToParent());
@@ -911,4 +923,116 @@ public static LlmAgent fromConfig(LlmAgentConfig config, String configAbsPath)
911923

912924
return agent;
913925
}
926+
927+
private static ImmutableList<BaseTool> resolveTools(
928+
List<ToolConfig> toolConfigs, String configAbsPath) throws ConfigurationException {
929+
930+
if (toolConfigs == null || toolConfigs.isEmpty()) {
931+
return ImmutableList.of();
932+
}
933+
934+
List<BaseTool> resolvedTools = new ArrayList<>();
935+
936+
for (ToolConfig toolConfig : toolConfigs) {
937+
try {
938+
String toolName = toolConfig.name();
939+
if (toolName == null || toolName.trim().isEmpty()) {
940+
throw new ConfigurationException("Tool name cannot be empty");
941+
}
942+
943+
toolName = toolName.trim();
944+
BaseTool tool;
945+
946+
if (!toolName.contains(".")) {
947+
tool = resolveBuiltInTool(toolName, toolConfig);
948+
} else {
949+
// TODO: Support user-defined tools
950+
logger.debug("configAbsPath is: {}", configAbsPath);
951+
throw new ConfigurationException("User-defined tools are not yet supported");
952+
}
953+
954+
resolvedTools.add(tool);
955+
logger.debug("Successfully resolved tool: {}", toolConfig.name());
956+
} catch (Exception e) {
957+
String errorMsg = "Failed to resolve tool: " + toolConfig.name();
958+
logger.error(errorMsg, e);
959+
throw new ConfigurationException(errorMsg, e);
960+
}
961+
}
962+
963+
return ImmutableList.copyOf(resolvedTools);
964+
}
965+
966+
private static BaseTool resolveBuiltInTool(String toolName, ToolConfig toolConfig)
967+
throws ConfigurationException {
968+
try {
969+
logger.debug("Resolving built-in tool: {}", toolName);
970+
// TODO: Handle built-in tool name end with Tool while config yaml file does not.
971+
// e.g.google_search in config yaml file and GoogleSearchTool in tool class name.
972+
String pascalCaseToolName = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, toolName);
973+
String className = "com.google.adk.tools." + pascalCaseToolName;
974+
Class<?> toolClass;
975+
try {
976+
toolClass = Class.forName(className);
977+
logger.debug("Successfully loaded tool class: {}", className);
978+
} catch (ClassNotFoundException e) {
979+
String fallbackClassName = "com.google.adk.tools." + toolName;
980+
try {
981+
toolClass = Class.forName(fallbackClassName);
982+
} catch (ClassNotFoundException e2) {
983+
throw new ConfigurationException(
984+
"Built-in tool not found: "
985+
+ toolName
986+
+ ". Expected class: "
987+
+ className
988+
+ " or "
989+
+ fallbackClassName,
990+
e2);
991+
}
992+
}
993+
994+
if (!BaseTool.class.isAssignableFrom(toolClass)) {
995+
throw new ConfigurationException(
996+
"Built-in tool class " + toolClass.getName() + " does not extend BaseTool");
997+
}
998+
999+
@SuppressWarnings("unchecked")
1000+
Class<? extends BaseTool> baseToolClass = (Class<? extends BaseTool>) toolClass;
1001+
1002+
BaseTool tool = createToolInstance(baseToolClass, toolConfig);
1003+
logger.info(
1004+
"Successfully created built-in tool: {} (class: {})", toolName, toolClass.getName());
1005+
1006+
return tool;
1007+
1008+
} catch (Exception e) {
1009+
logger.error("Failed to create built-in tool: {}", toolName, e);
1010+
throw new ConfigurationException("Failed to create built-in tool: " + toolName, e);
1011+
}
1012+
}
1013+
1014+
private static BaseTool createToolInstance(
1015+
Class<? extends BaseTool> toolClass, ToolConfig toolConfig)
1016+
throws ConfigAgentUtils.ConfigurationException {
1017+
1018+
try {
1019+
// TODO:implement constructor with ToolArgsConfig
1020+
logger.debug("ToolConfig is: {}", toolConfig);
1021+
1022+
// Try default constructor
1023+
try {
1024+
Constructor<? extends BaseTool> constructor = toolClass.getConstructor();
1025+
return constructor.newInstance();
1026+
} catch (NoSuchMethodException e) {
1027+
// Continue
1028+
}
1029+
1030+
throw new ConfigAgentUtils.ConfigurationException(
1031+
"No suitable constructor found for tool class: " + toolClass.getName());
1032+
1033+
} catch (Exception e) {
1034+
throw new ConfigAgentUtils.ConfigurationException(
1035+
"Failed to instantiate tool class: " + toolClass.getName(), e);
1036+
}
1037+
}
9141038
}

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package com.google.adk.agents;
1818

1919
import com.fasterxml.jackson.annotation.JsonProperty;
20+
import com.google.adk.tools.BaseTool.ToolConfig;
21+
import java.util.List;
2022

2123
/**
2224
* Configuration for LlmAgent.
@@ -29,6 +31,7 @@ public class LlmAgentConfig extends BaseAgentConfig {
2931
private Boolean disallowTransferToParent;
3032
private Boolean disallowTransferToPeers;
3133
private String outputKey;
34+
private List<ToolConfig> tools;
3235

3336
public LlmAgentConfig() {
3437
super();
@@ -80,4 +83,13 @@ public String outputKey() {
8083
public void setOutputKey(String outputKey) {
8184
this.outputKey = outputKey;
8285
}
86+
87+
@JsonProperty("tools")
88+
public List<ToolConfig> tools() {
89+
return tools;
90+
}
91+
92+
public void setTools(List<ToolConfig> tools) {
93+
this.tools = tools;
94+
}
8395
}

core/src/main/java/com/google/adk/tools/BaseTool.java

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,20 @@
1818

1919
import static com.google.common.collect.ImmutableList.toImmutableList;
2020

21+
import com.fasterxml.jackson.annotation.JsonIgnore;
22+
import com.google.adk.JsonBaseModel;
23+
import com.google.adk.agents.ConfigAgentUtils.ConfigurationException;
2124
import com.google.adk.models.LlmRequest;
2225
import com.google.common.collect.ImmutableList;
2326
import com.google.errorprone.annotations.CanIgnoreReturnValue;
27+
import com.google.errorprone.annotations.DoNotCall;
2428
import com.google.genai.types.FunctionDeclaration;
2529
import com.google.genai.types.GenerateContentConfig;
2630
import com.google.genai.types.LiveConnectConfig;
2731
import com.google.genai.types.Tool;
2832
import io.reactivex.rxjava3.core.Completable;
2933
import io.reactivex.rxjava3.core.Single;
34+
import java.util.HashMap;
3035
import java.util.Map;
3136
import java.util.Optional;
3237
import javax.annotation.Nonnull;
@@ -156,4 +161,66 @@ private static ImmutableList<Tool> findToolsWithoutFunctionDeclarations(LlmReque
156161
.collect(toImmutableList()))
157162
.orElse(ImmutableList.of());
158163
}
164+
165+
/**
166+
* Creates a tool instance from a config.
167+
*
168+
* <p>Subclasses should override and implement this method to do custom initialization from a
169+
* config.
170+
*
171+
* @param config The config for the tool.
172+
* @param configAbsPath The absolute path to the config file that contains the tool config.
173+
* @return The tool instance.
174+
* @throws ConfigurationException if the tool cannot be created from the config.
175+
*/
176+
@DoNotCall("Always throws com.google.adk.agents.ConfigAgentUtils.ConfigurationException")
177+
public static BaseTool fromConfig(ToolConfig config, String configAbsPath)
178+
throws ConfigurationException {
179+
throw new ConfigurationException(
180+
"fromConfig not implemented for " + BaseTool.class.getSimpleName());
181+
}
182+
183+
/** Configuration class for tool arguments that allows arbitrary key-value pairs. */
184+
// TODO implement this class
185+
public static class ToolArgsConfig extends JsonBaseModel {
186+
187+
@JsonIgnore private final Map<String, Object> additionalProperties = new HashMap<>();
188+
189+
public boolean isEmpty() {
190+
return additionalProperties.isEmpty();
191+
}
192+
193+
public int size() {
194+
return additionalProperties.size();
195+
}
196+
}
197+
198+
/** Configuration class for a tool definition in YAML/JSON. */
199+
public static class ToolConfig extends JsonBaseModel {
200+
private String name;
201+
private ToolArgsConfig args;
202+
203+
public ToolConfig() {}
204+
205+
public ToolConfig(String name, ToolArgsConfig args) {
206+
this.name = name;
207+
this.args = args;
208+
}
209+
210+
public String name() {
211+
return name;
212+
}
213+
214+
public void setName(String name) {
215+
this.name = name;
216+
}
217+
218+
public ToolArgsConfig args() {
219+
return args;
220+
}
221+
222+
public void setArgs(ToolArgsConfig args) {
223+
this.args = args;
224+
}
225+
}
159226
}

core/src/test/java/com/google/adk/agents/ConfigAgentUtilsTest.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,55 @@ public void fromConfig_withEmptyModel_doesNotSetModelOnAgent()
219219
assertThat(llmAgent.model()).isEmpty();
220220
}
221221

222+
@Test
223+
public void fromConfig_withBuiltInTool_loadsTool() throws IOException, ConfigurationException {
224+
File configFile = tempFolder.newFile("with_tool.yaml");
225+
Files.writeString(
226+
configFile.toPath(),
227+
"""
228+
name: search_agent
229+
model: gemini-1.5-flash
230+
description: 'an agent whose job it is to perform Google search queries and answer questions about the results.'
231+
instruction: You are an agent whose job is to perform Google search queries and answer questions about the results.
232+
agent_class: LlmAgent
233+
tools:
234+
- name: GoogleSearchTool
235+
""");
236+
String configPath = configFile.getAbsolutePath();
237+
238+
BaseAgent agent = ConfigAgentUtils.fromConfig(configPath);
239+
240+
assertThat(agent).isInstanceOf(LlmAgent.class);
241+
LlmAgent llmAgent = (LlmAgent) agent;
242+
assertThat(llmAgent.tools()).hasSize(1);
243+
assertThat(llmAgent.tools().get(0).name()).isEqualTo("google_search");
244+
}
245+
246+
@Test
247+
public void fromConfig_withBuiltInTool_loadsToolWithUnderscore()
248+
throws IOException, ConfigurationException {
249+
File configFile = tempFolder.newFile("with_tool_underscore.yaml");
250+
Files.writeString(
251+
configFile.toPath(),
252+
"""
253+
name: search_agent
254+
model: gemini-1.5-flash
255+
description: 'an agent whose job it is to perform Google search queries and answer questions about the results.'
256+
instruction: You are an agent whose job is to perform Google search queries and answer questions about the results.
257+
agent_class: LlmAgent
258+
tools:
259+
- name: google_search_tool
260+
""");
261+
String configPath = configFile.getAbsolutePath();
262+
263+
BaseAgent agent = ConfigAgentUtils.fromConfig(configPath);
264+
265+
assertThat(agent).isInstanceOf(LlmAgent.class);
266+
LlmAgent llmAgent = (LlmAgent) agent;
267+
assertThat(llmAgent.tools()).hasSize(1);
268+
assertThat(llmAgent.tools().get(0).name()).isEqualTo("google_search");
269+
}
270+
222271
@Test
223272
public void fromConfig_withInvalidModel_throwsExceptionOnModelResolution()
224273
throws IOException, ConfigurationException {

0 commit comments

Comments
 (0)