Skip to content
Merged
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
132 changes: 128 additions & 4 deletions core/src/main/java/com/google/adk/agents/LlmAgent.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import com.google.adk.agents.Callbacks.BeforeToolCallback;
import com.google.adk.agents.Callbacks.BeforeToolCallbackBase;
import com.google.adk.agents.Callbacks.BeforeToolCallbackSync;
import com.google.adk.agents.ConfigAgentUtils.ConfigurationException;
import com.google.adk.events.Event;
import com.google.adk.examples.BaseExampleProvider;
import com.google.adk.examples.Example;
Expand All @@ -49,7 +50,9 @@
import com.google.adk.models.LlmRegistry;
import com.google.adk.models.Model;
import com.google.adk.tools.BaseTool;
import com.google.adk.tools.BaseTool.ToolConfig;
import com.google.adk.tools.BaseToolset;
import com.google.common.base.CaseFormat;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
Expand All @@ -59,6 +62,7 @@
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.rxjava3.core.Single;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -864,20 +868,20 @@ private Model resolveModelInternal() {
* @param configAbsPath The absolute path to the agent config file. This is needed for resolving
* relative paths for e.g. tools.
* @return the configured LlmAgent
* @throws ConfigAgentUtils.ConfigurationException if the configuration is invalid
* @throws ConfigurationException if the configuration is invalid
* <p>TODO: Config agent features are not yet ready for public use.
*/
public static LlmAgent fromConfig(LlmAgentConfig config, String configAbsPath)
throws ConfigAgentUtils.ConfigurationException {
throws ConfigurationException {
logger.debug("Creating LlmAgent from config: {}", config.name());

// Validate required fields
if (config.name() == null || config.name().trim().isEmpty()) {
throw new ConfigAgentUtils.ConfigurationException("Agent name is required");
throw new ConfigurationException("Agent name is required");
}

if (config.instruction() == null || config.instruction().trim().isEmpty()) {
throw new ConfigAgentUtils.ConfigurationException("Agent instruction is required");
throw new ConfigurationException("Agent instruction is required");
}

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

try {
if (config.tools() != null) {
builder.tools(resolveTools(config.tools(), configAbsPath));
}
} catch (ConfigurationException e) {
throw new ConfigurationException("Error resolving tools for agent " + config.name(), e);
}

// Set optional transfer configuration
if (config.disallowTransferToParent() != null) {
builder.disallowTransferToParent(config.disallowTransferToParent());
Expand All @@ -911,4 +923,116 @@ public static LlmAgent fromConfig(LlmAgentConfig config, String configAbsPath)

return agent;
}

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

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

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

for (ToolConfig toolConfig : toolConfigs) {
try {
String toolName = toolConfig.name();
if (toolName == null || toolName.trim().isEmpty()) {
throw new ConfigurationException("Tool name cannot be empty");
}

toolName = toolName.trim();
BaseTool tool;

if (!toolName.contains(".")) {
tool = resolveBuiltInTool(toolName, toolConfig);
} 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());
} catch (Exception e) {
String errorMsg = "Failed to resolve tool: " + toolConfig.name();
logger.error(errorMsg, e);
throw new ConfigurationException(errorMsg, e);
}
}

return ImmutableList.copyOf(resolvedTools);
}

private static BaseTool resolveBuiltInTool(String toolName, ToolConfig toolConfig)
throws ConfigurationException {
try {
logger.debug("Resolving built-in tool: {}", 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;
Class<?> toolClass;
try {
toolClass = Class.forName(className);
logger.debug("Successfully loaded tool 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: "
+ toolName
+ ". Expected class: "
+ className
+ " or "
+ fallbackClassName,
e2);
}
}

if (!BaseTool.class.isAssignableFrom(toolClass)) {
throw new ConfigurationException(
"Built-in tool class " + toolClass.getName() + " does not extend BaseTool");
}

@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);
}
}

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

try {
// TODO:implement constructor with ToolArgsConfig
logger.debug("ToolConfig is: {}", toolConfig);

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

throw new ConfigAgentUtils.ConfigurationException(
"No suitable constructor found for tool class: " + toolClass.getName());

} catch (Exception e) {
throw new ConfigAgentUtils.ConfigurationException(
"Failed to instantiate tool class: " + toolClass.getName(), e);
}
}
}
12 changes: 12 additions & 0 deletions core/src/main/java/com/google/adk/agents/LlmAgentConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
package com.google.adk.agents;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.adk.tools.BaseTool.ToolConfig;
import java.util.List;

/**
* Configuration for LlmAgent.
Expand All @@ -29,6 +31,7 @@ public class LlmAgentConfig extends BaseAgentConfig {
private Boolean disallowTransferToParent;
private Boolean disallowTransferToPeers;
private String outputKey;
private List<ToolConfig> tools;

public LlmAgentConfig() {
super();
Expand Down Expand Up @@ -80,4 +83,13 @@ public String outputKey() {
public void setOutputKey(String outputKey) {
this.outputKey = outputKey;
}

@JsonProperty("tools")
public List<ToolConfig> tools() {
return tools;
}

public void setTools(List<ToolConfig> tools) {
this.tools = tools;
}
}
67 changes: 67 additions & 0 deletions core/src/main/java/com/google/adk/tools/BaseTool.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,20 @@

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

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.google.adk.JsonBaseModel;
import com.google.adk.agents.ConfigAgentUtils.ConfigurationException;
import com.google.adk.models.LlmRequest;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.DoNotCall;
import com.google.genai.types.FunctionDeclaration;
import com.google.genai.types.GenerateContentConfig;
import com.google.genai.types.LiveConnectConfig;
import com.google.genai.types.Tool;
import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.core.Single;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Nonnull;
Expand Down Expand Up @@ -156,4 +161,66 @@ private static ImmutableList<Tool> findToolsWithoutFunctionDeclarations(LlmReque
.collect(toImmutableList()))
.orElse(ImmutableList.of());
}

/**
* Creates a tool instance from a config.
*
* <p>Subclasses should override and implement this method to do custom initialization from a
* config.
*
* @param config The config for the tool.
* @param configAbsPath The absolute path to the config file that contains the tool config.
* @return The tool instance.
* @throws ConfigurationException if the tool cannot be created from the config.
*/
@DoNotCall("Always throws com.google.adk.agents.ConfigAgentUtils.ConfigurationException")
public static BaseTool fromConfig(ToolConfig config, String configAbsPath)
throws ConfigurationException {
throw new ConfigurationException(
"fromConfig not implemented for " + BaseTool.class.getSimpleName());
}

/** Configuration class for tool arguments that allows arbitrary key-value pairs. */
// TODO implement this class
public static class ToolArgsConfig extends JsonBaseModel {

@JsonIgnore private final Map<String, Object> additionalProperties = new HashMap<>();

public boolean isEmpty() {
return additionalProperties.isEmpty();
}

public int size() {
return additionalProperties.size();
}
}

/** Configuration class for a tool definition in YAML/JSON. */
public static class ToolConfig extends JsonBaseModel {
private String name;
private ToolArgsConfig args;

public ToolConfig() {}

public ToolConfig(String name, ToolArgsConfig args) {
this.name = name;
this.args = args;
}

public String name() {
return name;
}

public void setName(String name) {
this.name = name;
}

public ToolArgsConfig args() {
return args;
}

public void setArgs(ToolArgsConfig args) {
this.args = args;
}
}
}
49 changes: 49 additions & 0 deletions core/src/test/java/com/google/adk/agents/ConfigAgentUtilsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,55 @@ public void fromConfig_withEmptyModel_doesNotSetModelOnAgent()
assertThat(llmAgent.model()).isEmpty();
}

@Test
public void fromConfig_withBuiltInTool_loadsTool() throws IOException, ConfigurationException {
File configFile = tempFolder.newFile("with_tool.yaml");
Files.writeString(
configFile.toPath(),
"""
name: search_agent
model: gemini-1.5-flash
description: 'an agent whose job it is to perform Google search queries and answer questions about the results.'
instruction: You are an agent whose job is to perform Google search queries and answer questions about the results.
agent_class: LlmAgent
tools:
- name: GoogleSearchTool
""");
String configPath = configFile.getAbsolutePath();

BaseAgent agent = ConfigAgentUtils.fromConfig(configPath);

assertThat(agent).isInstanceOf(LlmAgent.class);
LlmAgent llmAgent = (LlmAgent) agent;
assertThat(llmAgent.tools()).hasSize(1);
assertThat(llmAgent.tools().get(0).name()).isEqualTo("google_search");
}

@Test
public void fromConfig_withBuiltInTool_loadsToolWithUnderscore()
throws IOException, ConfigurationException {
File configFile = tempFolder.newFile("with_tool_underscore.yaml");
Files.writeString(
configFile.toPath(),
"""
name: search_agent
model: gemini-1.5-flash
description: 'an agent whose job it is to perform Google search queries and answer questions about the results.'
instruction: You are an agent whose job is to perform Google search queries and answer questions about the results.
agent_class: LlmAgent
tools:
- name: google_search_tool
""");
String configPath = configFile.getAbsolutePath();

BaseAgent agent = ConfigAgentUtils.fromConfig(configPath);

assertThat(agent).isInstanceOf(LlmAgent.class);
LlmAgent llmAgent = (LlmAgent) agent;
assertThat(llmAgent.tools()).hasSize(1);
assertThat(llmAgent.tools().get(0).name()).isEqualTo("google_search");
}

@Test
public void fromConfig_withInvalidModel_throwsExceptionOnModelResolution()
throws IOException, ConfigurationException {
Expand Down