52
52
import com .google .adk .models .LlmRegistry ;
53
53
import com .google .adk .models .Model ;
54
54
import com .google .adk .tools .BaseTool ;
55
+ import com .google .adk .tools .BaseTool .ToolArgsConfig ;
55
56
import com .google .adk .tools .BaseTool .ToolConfig ;
56
57
import com .google .adk .tools .BaseToolset ;
57
58
import com .google .adk .utils .ComponentRegistry ;
64
65
import io .reactivex .rxjava3 .core .Flowable ;
65
66
import io .reactivex .rxjava3 .core .Maybe ;
66
67
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 ;
67
72
import java .util .ArrayList ;
68
73
import java .util .List ;
69
74
import java .util .Map ;
@@ -939,14 +944,26 @@ public static LlmAgent fromConfig(LlmAgentConfig config, String configAbsPath)
939
944
return agent ;
940
945
}
941
946
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 {
944
961
945
962
if (toolConfigs == null || toolConfigs .isEmpty ()) {
946
963
return ImmutableList .of ();
947
964
}
948
965
949
- List <BaseTool > resolvedTools = new ArrayList <> ();
966
+ ImmutableList . Builder <BaseTool > resolvedTools = ImmutableList . builder ();
950
967
951
968
for (ToolConfig toolConfig : toolConfigs ) {
952
969
try {
@@ -955,24 +972,215 @@ private static ImmutableList<BaseTool> resolveTools(
955
972
}
956
973
957
974
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 ;
966
982
}
967
983
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
+
969
994
} catch (Exception e ) {
970
995
String errorMsg = "Failed to resolve tool: " + toolConfig .name ();
971
996
logger .error (errorMsg , e );
972
997
throw new ConfigurationException (errorMsg , e );
973
998
}
974
999
}
975
1000
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 ;
977
1185
}
978
1186
}
0 commit comments