Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 83 additions & 32 deletions core/src/main/java/com/google/adk/agents/LlmAgent.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.rxjava3.core.Single;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -782,6 +783,13 @@ public List<BaseTool> tools() {
return canonicalTools().toList().blockingGet();
}

public List<BaseToolset> toolsets() {
return toolsUnion.stream()
.filter(t -> t instanceof BaseToolset)
.map(t -> (BaseToolset) t)
.collect(ImmutableList.toImmutableList());
}

public List<Object> toolsUnion() {
return toolsUnion;
}
Expand Down Expand Up @@ -939,14 +947,14 @@ public static LlmAgent fromConfig(LlmAgentConfig config, String configAbsPath)
return agent;
}

private static ImmutableList<BaseTool> resolveTools(
private static ImmutableList<Object> resolveTools(
List<ToolConfig> toolConfigs, String configAbsPath) throws ConfigurationException {

if (toolConfigs == null || toolConfigs.isEmpty()) {
return ImmutableList.of();
}

List<BaseTool> resolvedTools = new ArrayList<>();
List<Object> resolvedTools = new ArrayList<>();

for (ToolConfig toolConfig : toolConfigs) {
try {
Expand All @@ -956,20 +964,20 @@ private static ImmutableList<BaseTool> resolveTools(
}

toolName = toolName.trim();
BaseTool tool;
Object toolOrToolset;

if (!toolName.contains(".")) {
tool = resolveBuiltInTool(toolName, toolConfig);
toolOrToolset = resolveBuiltInTool(toolName, toolConfig, configAbsPath);
} else {
// TODO: Support user-defined tools
logger.debug("configAbsPath is: {}", configAbsPath);
throw new ConfigurationException("User-defined tools are not yet supported");
}

resolvedTools.add(tool);
logger.debug("Successfully resolved tool: {}", toolConfig.name());
resolvedTools.add(toolOrToolset);
logger.debug("Successfully resolved tool/toolset: {}", toolConfig.name());
} catch (Exception e) {
String errorMsg = "Failed to resolve tool: " + toolConfig.name();
String errorMsg = "Failed to resolve tool/toolset: " + toolConfig.name();
logger.error(errorMsg, e);
throw new ConfigurationException(errorMsg, e);
}
Expand All @@ -978,25 +986,29 @@ private static ImmutableList<BaseTool> resolveTools(
return ImmutableList.copyOf(resolvedTools);
}

private static BaseTool resolveBuiltInTool(String toolName, ToolConfig toolConfig)
throws ConfigurationException {
private static Object resolveBuiltInTool(
String toolName, ToolConfig toolConfig, String configAbsPath) throws ConfigurationException {
try {
logger.debug("Resolving built-in tool: {}", toolName);
logger.debug("Resolving built-in tool/toolset: {}", toolName);
// TODO: Handle built-in tool name end with Tool while config yaml file does not.
// e.g.google_search in config yaml file and GoogleSearchTool in tool class name.
String pascalCaseToolName = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, toolName);
String className = "com.google.adk.tools." + pascalCaseToolName;
// TODO: use tool registry to handle this instead of hardcoding.
if (toolName.equals("McpToolset")) {
className = "com.google.adk.tools.mcp.McpToolset";
}
Class<?> toolClass;
try {
toolClass = Class.forName(className);
logger.debug("Successfully loaded tool class: {}", className);
logger.debug("Successfully loaded tool/toolset class: {}", className);
} catch (ClassNotFoundException e) {
String fallbackClassName = "com.google.adk.tools." + toolName;
try {
toolClass = Class.forName(fallbackClassName);
} catch (ClassNotFoundException e2) {
throw new ConfigurationException(
"Built-in tool not found: "
"Built-in tool/toolset not found: "
+ toolName
+ ". Expected class: "
+ className
Expand All @@ -1006,48 +1018,87 @@ private static BaseTool resolveBuiltInTool(String toolName, ToolConfig toolConfi
}
}

if (!BaseTool.class.isAssignableFrom(toolClass)) {
if (BaseTool.class.isAssignableFrom(toolClass)) {
logger.debug("Tool {} is a sub-class of BaseTool.", toolConfig.name());
@SuppressWarnings("unchecked")
Class<? extends BaseTool> baseToolClass = (Class<? extends BaseTool>) toolClass;
BaseTool tool = createToolInstance(baseToolClass, toolConfig, configAbsPath);
logger.info(
"Successfully created built-in tool: {} (class: {})", toolName, toolClass.getName());
return tool;
} else if (BaseToolset.class.isAssignableFrom(toolClass)) {
logger.debug("Tool {} is a sub-class of BaseToolset.", toolConfig.name());
@SuppressWarnings("unchecked")
Class<? extends BaseToolset> baseToolsetClass = (Class<? extends BaseToolset>) toolClass;
BaseToolset toolset = createToolsetInstance(baseToolsetClass, toolConfig, configAbsPath);
logger.info(
"Successfully created built-in toolset: {} (class: {})", toolName, toolClass.getName());
return toolset;
} else {
logger.info("configAbsPath is: {}", configAbsPath);
throw new ConfigurationException(
"Built-in tool class " + toolClass.getName() + " does not extend BaseTool");
"Built-in tool class "
+ toolClass.getName()
+ " does not extend BaseTool or BaseToolset");
}

@SuppressWarnings("unchecked")
Class<? extends BaseTool> baseToolClass = (Class<? extends BaseTool>) toolClass;

BaseTool tool = createToolInstance(baseToolClass, toolConfig);
logger.info(
"Successfully created built-in tool: {} (class: {})", toolName, toolClass.getName());

return tool;

} catch (Exception e) {
logger.error("Failed to create built-in tool: {}", toolName, e);
throw new ConfigurationException("Failed to create built-in tool: " + toolName, e);
logger.error("Failed to create built-in tool/toolset: {}", toolName, e);
throw new ConfigurationException("Failed to create built-in tool/toolset: " + toolName, e);
}
}

private static BaseTool createToolInstance(
Class<? extends BaseTool> toolClass, ToolConfig toolConfig)
Class<? extends BaseTool> toolClass, ToolConfig toolConfig, String configAbsPath)
throws ConfigAgentUtils.ConfigurationException {

try {
// TODO:implement constructor with ToolArgsConfig
logger.debug("ToolConfig is: {}", toolConfig);
// First, try to use the fromConfig static method
try {
Method fromConfigMethod = toolClass.getMethod("fromConfig", ToolConfig.class, String.class);
return (BaseTool) fromConfigMethod.invoke(null, toolConfig, configAbsPath);
} catch (ReflectiveOperationException e) {
// Continue
}

// Try default constructor
// Second, try default constructor
try {
Constructor<? extends BaseTool> constructor = toolClass.getConstructor();
return constructor.newInstance();
Constructor<? extends BaseTool> defaultConstructor = toolClass.getConstructor();
return defaultConstructor.newInstance();
} catch (NoSuchMethodException e) {
// Continue
}

// Third, try constructor with ToolArgsConfig
// TODO: implement constructor with ToolArgsConfig

logger.debug("ToolConfig is: {}", toolConfig);
logger.debug("configAbsPath is: {}", configAbsPath);
throw new ConfigAgentUtils.ConfigurationException(
"No suitable constructor found for tool class: " + toolClass.getName());
"No suitable constructor or fromConfig method found for tool class: "
+ toolClass.getName());

} catch (ConfigAgentUtils.ConfigurationException e) {
throw e;
} catch (Exception e) {
throw new ConfigAgentUtils.ConfigurationException(
"Failed to instantiate tool class: " + toolClass.getName(), e);
}
}

private static BaseToolset createToolsetInstance(
Class<? extends BaseToolset> toolsetClass, ToolConfig toolConfig, String configAbsPath)
throws ConfigAgentUtils.ConfigurationException {
try {
Method fromConfigMethod =
toolsetClass.getMethod("fromConfig", ToolConfig.class, String.class);
return (BaseToolset) fromConfigMethod.invoke(null, toolConfig, configAbsPath);
} catch (ReflectiveOperationException e) {
throw new ConfigAgentUtils.ConfigurationException(
"No suitable fromConfig method found for toolset class: " + toolsetClass.getName(), e);
} catch (RuntimeException e) {
throw new ConfigAgentUtils.ConfigurationException(
"Failed to instantiate toolset class: " + toolsetClass.getName(), e);
}
}
}
44 changes: 42 additions & 2 deletions core/src/main/java/com/google/adk/tools/BaseTool.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

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

import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.google.adk.JsonBaseModel;
import com.google.adk.agents.ConfigAgentUtils.ConfigurationException;
Expand Down Expand Up @@ -180,8 +182,12 @@ public static BaseTool fromConfig(ToolConfig config, String configAbsPath)
"fromConfig not implemented for " + BaseTool.class.getSimpleName());
}

/** Configuration class for tool arguments that allows arbitrary key-value pairs. */
// TODO implement this class
/**
* Configuration class for tool arguments that allows arbitrary key-value pairs.
*
* <p>This class is used to parse tool arguments from a YAML configuration file. It supports
* arbitrary key-value pairs, which can be accessed using the {@link #get(String)} method.
*/
public static class ToolArgsConfig extends JsonBaseModel {

@JsonIgnore private final Map<String, Object> additionalProperties = new HashMap<>();
Expand All @@ -193,6 +199,40 @@ public boolean isEmpty() {
public int size() {
return additionalProperties.size();
}

@JsonAnyGetter
public Map<String, Object> getAdditionalProperties() {
return additionalProperties;
}

@JsonAnySetter
public void setAdditionalProperty(String name, Object value) {
additionalProperties.put(name, value);
}

public @Nullable Object get(String key) {
return additionalProperties.get(key);
}

public <T> @Nullable T get(String key, Class<T> type) {
Object value = additionalProperties.get(key);
if (value == null) {
return null;
}
return type.cast(value);
}

public void put(String key, Object value) {
additionalProperties.put(key, value);
}

public Object remove(String key) {
return additionalProperties.remove(key);
}

public boolean containsKey(String key) {
return additionalProperties.containsKey(key);
}
}

/** Configuration class for a tool definition in YAML/JSON. */
Expand Down
67 changes: 62 additions & 5 deletions core/src/main/java/com/google/adk/tools/BaseToolset.java
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
package com.google.adk.tools;

import com.google.adk.agents.ConfigAgentUtils.ConfigurationException;
import com.google.adk.agents.ReadonlyContext;
import com.google.adk.tools.BaseTool.ToolConfig;
import com.google.common.base.VerifyException;
import com.google.errorprone.annotations.DoNotCall;
import io.reactivex.rxjava3.core.Flowable;
import java.util.List;
import java.util.Optional;

/** Base interface for toolsets. */
public interface BaseToolset extends AutoCloseable {
/** Base abstract class for toolsets. */
public abstract class BaseToolset implements AutoCloseable {

/**
* The configuration class type for this toolset.
*
* <p>Subclasses can provide their own configuration type by declaring their own {@code static
* final CONFIG_TYPE} field. The {@link #getConfigType()} method is designed to correctly retrieve
* the value from the subclass.
*/
protected static final Class<?> CONFIG_TYPE = Object.class;

/**
* Return all tools in the toolset based on the provided context.
*
* @param readonlyContext Context used to filter tools available to the agent.
* @return A Single emitting a list of tools available under the specified context.
*/
Flowable<BaseTool> getTools(ReadonlyContext readonlyContext);
public abstract Flowable<BaseTool> getTools(ReadonlyContext readonlyContext);

/**
* Performs cleanup and releases resources held by the toolset.
Expand All @@ -24,7 +37,33 @@ public interface BaseToolset extends AutoCloseable {
* files, or other managed resources are properly released to prevent leaks.
*/
@Override
void close() throws Exception;
public abstract void close() throws Exception;

/**
* Gets the config type for this toolset class, supporting inheritance. Subclasses can override
* the CONFIG_TYPE field to use their own config class.
*/
public Class<?> getConfigType() {
try {
// First try to get the field from the current class
return (Class<?>) this.getClass().getDeclaredField("CONFIG_TYPE").get(null);
} catch (NoSuchFieldException e) {
// If subclass doesn't declare it, walk up to parent class
Class<?> superclass = this.getClass().getSuperclass();
while (superclass != null) {
try {
return (Class<?>) superclass.getDeclaredField("CONFIG_TYPE").get(null);
} catch (NoSuchFieldException ex) {
superclass = superclass.getSuperclass();
} catch (IllegalAccessException ex) {
throw new VerifyException("Cannot access CONFIG_TYPE field", ex);
}
}
throw new IllegalStateException("No CONFIG_TYPE field found in class hierarchy", e);
} catch (IllegalAccessException e) {
throw new VerifyException("Cannot access CONFIG_TYPE field", e);
}
}

/**
* Helper method to be used by implementers that returns true if the given tool is in the provided
Expand All @@ -35,7 +74,7 @@ public interface BaseToolset extends AutoCloseable {
* @param readonlyContext The current context.
* @return true if the tool is selected.
*/
default boolean isToolSelected(
protected boolean isToolSelected(
BaseTool tool, Optional<Object> toolFilter, Optional<ReadonlyContext> readonlyContext) {
if (toolFilter.isEmpty()) {
return true;
Expand All @@ -51,4 +90,22 @@ default boolean isToolSelected(
}
return false;
}

/**
* Creates a toolset instance from a config.
*
* <p>Concrete classes implementing BaseToolset should provide their own static fromConfig method.
* This method is here to guide the structure.
*
* @param config The config for the toolset.
* @param configAbsPath The absolute path to the config file that contains the toolset config.
* @return The toolset instance.
* @throws ConfigurationException if the toolset cannot be created from the config.
*/
@DoNotCall("Always throws com.google.adk.agents.ConfigAgentUtils.ConfigurationException")
public static BaseToolset fromConfig(ToolConfig config, String configAbsPath)
throws ConfigurationException {
throw new ConfigurationException(
"fromConfig not implemented for " + BaseToolset.class.getSimpleName());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import org.jspecify.annotations.Nullable;

/** Application Integration Toolset */
public class ApplicationIntegrationToolset implements BaseToolset {
public class ApplicationIntegrationToolset extends BaseToolset {
String project;
String location;
@Nullable String integration;
Expand Down
Loading