Skip to content

Commit a2d9533

Browse files
Doris26copybara-github
authored andcommitted
feat: Implement automatic tool discovery for config-based agents
This change allows `ConfigAgentLoader` to scan agent YAML files for tool names that are fully qualified Java class or method/field references. It automatically registers these tools in the `ComponentRegistry`. The following patterns for tool names in the YAML are supported: 1. **Class Name**: e.g., `com.example.MyTool` * LlmAgent will instantiate this class. * If `args` are provided in the YAML, a static `fromConfig(ToolArgsConfig)` method must exist. * Otherwise, the class must have a default constructor. * This is typically used for tools extending `BaseTool`. 2. **Fully Qualified Static Field**: e.g., `com.example.MyTool.MY_TOOL_INSTANCE` * LlmAgent will resolve the static field `MY_TOOL_INSTANCE` from the class `com.example.MyTool`. * The field must be of a type assignable to `BaseTool`. 3. **Registry Name**: e.g., `my_custom_tool` * The tool must be pre-registered in the `ComponentRegistry`. * A custom `ComponentRegistry` can be specified via the system property `-Dregistry=com.example.CustomRegistry`. Usage Example: ```bash # Navigate to the example directory cd maven_plugin/examples/custom_tools # Clean, compile, and start the server mvn clean compile google-adk:web \ -Dagents=config_agents \ -Dregistry=com.example.CustomDieRegistry ``` PiperOrigin-RevId: 799781730
1 parent d8a6457 commit a2d9533

File tree

13 files changed

+732
-173
lines changed

13 files changed

+732
-173
lines changed

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

Lines changed: 221 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import com.google.adk.models.LlmRegistry;
5353
import com.google.adk.models.Model;
5454
import com.google.adk.tools.BaseTool;
55+
import com.google.adk.tools.BaseTool.ToolArgsConfig;
5556
import com.google.adk.tools.BaseTool.ToolConfig;
5657
import com.google.adk.tools.BaseToolset;
5758
import com.google.adk.utils.ComponentRegistry;
@@ -64,6 +65,10 @@
6465
import io.reactivex.rxjava3.core.Flowable;
6566
import io.reactivex.rxjava3.core.Maybe;
6667
import io.reactivex.rxjava3.core.Single;
68+
import java.lang.reflect.Constructor;
69+
import java.lang.reflect.Field;
70+
import java.lang.reflect.Method;
71+
import java.lang.reflect.Modifier;
6772
import java.util.ArrayList;
6873
import java.util.List;
6974
import java.util.Map;
@@ -939,14 +944,26 @@ public static LlmAgent fromConfig(LlmAgentConfig config, String configAbsPath)
939944
return agent;
940945
}
941946

942-
private static ImmutableList<BaseTool> resolveTools(
943-
List<ToolConfig> toolConfigs, String configAbsPath) throws ConfigurationException {
947+
/**
948+
* Resolves a list of tool configurations into {@link BaseTool} instances.
949+
*
950+
* <p>This method is only for use by Agent Development Kit.
951+
*
952+
* @param toolConfigs The list of tool configurations to resolve.
953+
* @param configAbsPath The absolute path to the agent config file currently being processed. This
954+
* path can be used to resolve relative paths for tool configurations, if necessary.
955+
* @return An immutable list of resolved {@link BaseTool} instances.
956+
* @throws ConfigurationException if any tool configuration is invalid (e.g., missing name), if a
957+
* tool cannot be found by its name or class, or if tool instantiation fails.
958+
*/
959+
static ImmutableList<BaseTool> resolveTools(List<ToolConfig> toolConfigs, String configAbsPath)
960+
throws ConfigurationException {
944961

945962
if (toolConfigs == null || toolConfigs.isEmpty()) {
946963
return ImmutableList.of();
947964
}
948965

949-
List<BaseTool> resolvedTools = new ArrayList<>();
966+
ImmutableList.Builder<BaseTool> resolvedTools = ImmutableList.builder();
950967

951968
for (ToolConfig toolConfig : toolConfigs) {
952969
try {
@@ -955,24 +972,215 @@ private static ImmutableList<BaseTool> resolveTools(
955972
}
956973

957974
String toolName = toolConfig.name().trim();
958-
Optional<BaseTool> toolOpt = ComponentRegistry.resolveToolInstance(toolName);
959-
if (toolOpt.isPresent()) {
960-
resolvedTools.add(toolOpt.get());
961-
} else {
962-
// TODO: Support user-defined tools
963-
// TODO: Support using tool class via ComponentRegistry.resolveToolClass
964-
logger.debug("configAbsPath is: {}", configAbsPath);
965-
throw new ConfigurationException("Tool not found: " + toolName);
975+
976+
// Option 1: Try to resolve as a tool instance
977+
BaseTool tool = resolveToolInstance(toolName);
978+
if (tool != null) {
979+
resolvedTools.add(tool);
980+
logger.debug("Successfully resolved tool instance: {}", toolName);
981+
continue;
966982
}
967983

968-
logger.debug("Successfully resolved tool: {}", toolConfig.name());
984+
// Option 2: Try to resolve as a tool class (with or without args)
985+
BaseTool toolFromClass = resolveToolFromClass(toolName, toolConfig.args());
986+
if (toolFromClass != null) {
987+
resolvedTools.add(toolFromClass);
988+
logger.debug("Successfully resolved tool from class: {}", toolName);
989+
continue;
990+
}
991+
992+
throw new ConfigurationException("Tool not found: " + toolName);
993+
969994
} catch (Exception e) {
970995
String errorMsg = "Failed to resolve tool: " + toolConfig.name();
971996
logger.error(errorMsg, e);
972997
throw new ConfigurationException(errorMsg, e);
973998
}
974999
}
9751000

976-
return ImmutableList.copyOf(resolvedTools);
1001+
return resolvedTools.build();
1002+
}
1003+
1004+
/**
1005+
* Resolves a tool instance by its unique name or its static field reference.
1006+
*
1007+
* <p>It first checks the {@link ComponentRegistry} for a registered tool instance. If not found,
1008+
* and the name looks like a fully qualified Java name referencing a static field (e.g.,
1009+
* "com.google.mytools.MyToolClass.INSTANCE"), it attempts to resolve it via reflection using
1010+
* {@link #resolveInstanceViaReflection(String)}.
1011+
*
1012+
* @param toolName The name of the tool or a static field reference (e.g., "myTool",
1013+
* "com.google.mytools.MyToolClass.INSTANCE").
1014+
* @return The resolved tool instance, or {@code null} if the tool is not found in the registry
1015+
* and cannot be resolved via reflection.
1016+
*/
1017+
@Nullable
1018+
static BaseTool resolveToolInstance(String toolName) {
1019+
ComponentRegistry registry = ComponentRegistry.getInstance();
1020+
1021+
// First try registry
1022+
Optional<BaseTool> toolOpt = ComponentRegistry.resolveToolInstance(toolName);
1023+
if (toolOpt.isPresent()) {
1024+
return toolOpt.get();
1025+
}
1026+
1027+
// If not in registry and looks like Java qualified name, try reflection
1028+
if (isJavaQualifiedName(toolName)) {
1029+
try {
1030+
BaseTool tool = resolveInstanceViaReflection(toolName);
1031+
if (tool != null) {
1032+
registry.register(toolName, tool);
1033+
logger.debug("Resolved and registered tool instance via reflection: {}", toolName);
1034+
return tool;
1035+
}
1036+
} catch (Exception e) {
1037+
logger.debug("Failed to resolve instance via reflection: {}", toolName, e);
1038+
}
1039+
}
1040+
logger.debug("Could not resolve tool instance: {}", toolName);
1041+
return null;
1042+
}
1043+
1044+
/**
1045+
* Resolves a tool from a class name and optional arguments.
1046+
*
1047+
* <p>It attempts to load the class specified by {@code className}. If {@code args} are provided
1048+
* and non-empty, it looks for a static factory method {@code fromConfig(ToolArgsConfig)} on the
1049+
* class to instantiate the tool. If {@code args} are null or empty, it looks for a default
1050+
* constructor.
1051+
*
1052+
* @param className The fully qualified name of the tool class to instantiate.
1053+
* @param args Optional configuration arguments for tool creation. If provided, the class must
1054+
* implement a static {@code fromConfig(ToolArgsConfig)} factory method. If null or empty, the
1055+
* class must have a default constructor.
1056+
* @return The instantiated tool instance, or {@code null} if the class cannot be found or loaded.
1057+
* @throws ConfigurationException if {@code args} are provided but no {@code fromConfig} method
1058+
* exists, if {@code args} are not provided but no default constructor exists, or if
1059+
* instantiation via the factory method or constructor fails.
1060+
*/
1061+
@Nullable
1062+
static BaseTool resolveToolFromClass(String className, ToolArgsConfig args)
1063+
throws ConfigurationException {
1064+
ComponentRegistry registry = ComponentRegistry.getInstance();
1065+
1066+
// First try registry for class
1067+
Optional<Class<? extends BaseTool>> classOpt = ComponentRegistry.resolveToolClass(className);
1068+
Class<? extends BaseTool> toolClass = null;
1069+
1070+
if (classOpt.isPresent()) {
1071+
toolClass = classOpt.get();
1072+
} else if (isJavaQualifiedName(className)) {
1073+
// Try reflection to get class
1074+
try {
1075+
Class<?> clazz = Thread.currentThread().getContextClassLoader().loadClass(className);
1076+
if (BaseTool.class.isAssignableFrom(clazz)) {
1077+
toolClass = clazz.asSubclass(BaseTool.class);
1078+
// Optimization: register for reuse
1079+
registry.register(className, toolClass);
1080+
logger.debug("Resolved and registered tool class via reflection: {}", className);
1081+
}
1082+
} catch (ClassNotFoundException e) {
1083+
logger.debug("Failed to resolve class via reflection: {}", className, e);
1084+
return null;
1085+
}
1086+
}
1087+
1088+
if (toolClass == null) {
1089+
return null;
1090+
}
1091+
1092+
// If args provided and not empty, try fromConfig method first
1093+
if (args != null && !args.isEmpty()) {
1094+
try {
1095+
Method fromConfigMethod = toolClass.getMethod("fromConfig", ToolArgsConfig.class);
1096+
Object instance = fromConfigMethod.invoke(null, args);
1097+
if (instance instanceof BaseTool baseTool) {
1098+
return baseTool;
1099+
}
1100+
} catch (NoSuchMethodException e) {
1101+
throw new ConfigurationException(
1102+
"Class " + className + " does not have fromConfig method but args were provided.", e);
1103+
} catch (Exception e) {
1104+
logger.error("Error calling fromConfig on class {}", className, e);
1105+
throw new ConfigurationException("Error creating tool from class " + className, e);
1106+
}
1107+
}
1108+
1109+
// No args provided or empty args, try default constructor
1110+
try {
1111+
Constructor<? extends BaseTool> constructor = toolClass.getDeclaredConstructor();
1112+
constructor.setAccessible(true);
1113+
return constructor.newInstance();
1114+
} catch (NoSuchMethodException e) {
1115+
throw new ConfigurationException(
1116+
"Class " + className + " does not have a default constructor and no args were provided.",
1117+
e);
1118+
} catch (Exception e) {
1119+
logger.error("Error calling default constructor on class {}", className, e);
1120+
throw new ConfigurationException(
1121+
"Error creating tool from class " + className + " using default constructor", e);
1122+
}
1123+
}
1124+
1125+
/**
1126+
* Checks if a string appears to be a Java fully qualified name, such as "com.google.adk.MyClass"
1127+
* or "com.google.adk.MyClass.MY_FIELD".
1128+
*
1129+
* <p>It verifies that the name contains at least one dot ('.') and consists of characters valid
1130+
* for Java identifiers and package names.
1131+
*
1132+
* @param name The string to check.
1133+
* @return {@code true} if the string matches the pattern of a Java qualified name, {@code false}
1134+
* otherwise.
1135+
*/
1136+
static boolean isJavaQualifiedName(String name) {
1137+
if (name == null || name.trim().isEmpty()) {
1138+
return false;
1139+
}
1140+
return name.contains(".") && name.matches("^[a-zA-Z_$][a-zA-Z0-9_.$]*$");
1141+
}
1142+
1143+
/**
1144+
* Resolves a {@link BaseTool} instance by attempting to access a public static field via
1145+
* reflection.
1146+
*
1147+
* <p>This method expects {@code toolName} to be in the format
1148+
* "com.google.package.ClassName.STATIC_FIELD_NAME", where "STATIC_FIELD_NAME" is the name of a
1149+
* public static field in "com.google.package.ClassName" that holds a {@link BaseTool} instance.
1150+
*
1151+
* @param toolName The fully qualified name of a static field holding a tool instance.
1152+
* @return The {@link BaseTool} instance, or {@code null} if {@code toolName} is not in the
1153+
* expected format, or if the field is not found, not static, or not of type {@link BaseTool}.
1154+
* @throws Exception if the class specified in {@code toolName} cannot be loaded, or if there is a
1155+
* security manager preventing reflection, or if accessing the field causes an exception.
1156+
*/
1157+
@Nullable
1158+
static BaseTool resolveInstanceViaReflection(String toolName) throws Exception {
1159+
int lastDotIndex = toolName.lastIndexOf('.');
1160+
if (lastDotIndex == -1) {
1161+
return null;
1162+
}
1163+
1164+
String className = toolName.substring(0, lastDotIndex);
1165+
String fieldName = toolName.substring(lastDotIndex + 1);
1166+
1167+
Class<?> clazz = Thread.currentThread().getContextClassLoader().loadClass(className);
1168+
1169+
try {
1170+
Field field = clazz.getField(fieldName);
1171+
if (!Modifier.isStatic(field.getModifiers())) {
1172+
logger.debug("Field {} in class {} is not static", fieldName, className);
1173+
return null;
1174+
}
1175+
Object instance = field.get(null);
1176+
if (instance instanceof BaseTool baseTool) {
1177+
return baseTool;
1178+
} else {
1179+
logger.debug("Field {} in class {} is not a BaseTool instance", fieldName, className);
1180+
}
1181+
} catch (NoSuchFieldException e) {
1182+
logger.debug("Field {} not found in class {}", fieldName, className);
1183+
}
1184+
return null;
9771185
}
9781186
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,16 @@ public boolean isEmpty() {
193193
public int size() {
194194
return additionalProperties.size();
195195
}
196+
197+
@CanIgnoreReturnValue
198+
public ToolArgsConfig put(String key, Object value) {
199+
additionalProperties.put(key, value);
200+
return this;
201+
}
202+
203+
public Object get(String key) {
204+
return additionalProperties.get(key);
205+
}
196206
}
197207

198208
/** Configuration class for a tool definition in YAML/JSON. */

core/src/main/java/com/google/adk/utils/ComponentRegistry.java

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

1919
import static com.google.common.base.Strings.isNullOrEmpty;
20+
import static com.google.common.collect.ImmutableSet.toImmutableSet;
2021

2122
import com.google.adk.agents.BaseAgent;
2223
import com.google.adk.agents.LlmAgent;
@@ -30,6 +31,7 @@
3031
import com.google.adk.tools.LoadArtifactsTool;
3132
import java.util.Map;
3233
import java.util.Optional;
34+
import java.util.Set;
3335
import java.util.concurrent.ConcurrentHashMap;
3436
import javax.annotation.Nonnull;
3537
import org.slf4j.Logger;
@@ -325,4 +327,10 @@ public static Optional<Class<? extends BaseTool>> resolveToolClass(String toolCl
325327

326328
return Optional.empty();
327329
}
330+
331+
public Set<String> getToolNamesWithPrefix(String prefix) {
332+
return registry.keySet().stream()
333+
.filter(name -> name.startsWith(prefix))
334+
.collect(toImmutableSet());
335+
}
328336
}

0 commit comments

Comments
 (0)