Skip to content

Commit c5cbc6d

Browse files
Mateusz Krawieccopybara-github
authored andcommitted
feat: AgentTool.fromConfig()
PiperOrigin-RevId: 844728770
1 parent 007e938 commit c5cbc6d

File tree

8 files changed

+175
-87
lines changed

8 files changed

+175
-87
lines changed

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

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ static ImmutableList<Object> resolveToolsAndToolsets(
7474
String toolName = toolConfig.name().trim();
7575

7676
// First try to resolve as a toolset
77-
BaseToolset toolset = resolveToolsetFromClass(toolName, toolConfig.args());
77+
BaseToolset toolset = resolveToolsetFromClass(toolName, toolConfig.args(), configAbsPath);
7878
if (toolset != null) {
7979
resolvedItems.add(toolset);
8080
logger.debug("Successfully resolved toolset from class: {}", toolName);
@@ -90,7 +90,7 @@ static ImmutableList<Object> resolveToolsAndToolsets(
9090
}
9191

9292
// Option 2: Try to resolve as a tool class (with or without args)
93-
BaseTool toolFromClass = resolveToolFromClass(toolName, toolConfig.args());
93+
BaseTool toolFromClass = resolveToolFromClass(toolName, toolConfig.args(), configAbsPath);
9494
if (toolFromClass != null) {
9595
resolvedItems.add(toolFromClass);
9696
logger.debug("Successfully resolved tool from class: {}", toolName);
@@ -147,7 +147,7 @@ static ImmutableList<BaseTool> resolveTools(List<ToolConfig> toolConfigs, String
147147
}
148148

149149
// Option 2: Try to resolve as a tool class (with or without args)
150-
BaseTool toolFromClass = resolveToolFromClass(toolName, toolConfig.args());
150+
BaseTool toolFromClass = resolveToolFromClass(toolName, toolConfig.args(), configAbsPath);
151151
if (toolFromClass != null) {
152152
resolvedTools.add(toolFromClass);
153153
logger.debug("Successfully resolved tool from class: {}", toolName);
@@ -258,8 +258,8 @@ static BaseToolset resolveToolsetInstance(String toolsetName) {
258258
* @throws ConfigurationException if toolset instantiation fails.
259259
*/
260260
@Nullable
261-
static BaseToolset resolveToolsetFromClass(String className, ToolArgsConfig args)
262-
throws ConfigurationException {
261+
static BaseToolset resolveToolsetFromClass(
262+
String className, ToolArgsConfig args, String configAbsPath) throws ConfigurationException {
263263
ComponentRegistry registry = ComponentRegistry.getInstance();
264264

265265
// First try registry for class
@@ -383,7 +383,7 @@ static BaseToolset resolveToolsetInstanceViaReflection(String toolsetName) throw
383383
* instantiation via the factory method or constructor fails.
384384
*/
385385
@Nullable
386-
static BaseTool resolveToolFromClass(String className, ToolArgsConfig args)
386+
static BaseTool resolveToolFromClass(String className, ToolArgsConfig args, String configAbsPath)
387387
throws ConfigurationException {
388388
ComponentRegistry registry = ComponentRegistry.getInstance();
389389

@@ -416,8 +416,9 @@ static BaseTool resolveToolFromClass(String className, ToolArgsConfig args)
416416
// If args provided and not empty, try fromConfig method first
417417
if (args != null && !args.isEmpty()) {
418418
try {
419-
Method fromConfigMethod = toolClass.getMethod("fromConfig", ToolArgsConfig.class);
420-
Object instance = fromConfigMethod.invoke(null, args);
419+
Method fromConfigMethod =
420+
toolClass.getMethod("fromConfig", ToolArgsConfig.class, String.class);
421+
Object instance = fromConfigMethod.invoke(null, args, configAbsPath);
421422
if (instance instanceof BaseTool baseTool) {
422423
return baseTool;
423424
}

core/src/main/java/com/google/adk/examples/Example.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,22 @@
1616

1717
package com.google.adk.examples;
1818

19+
import com.fasterxml.jackson.annotation.JsonCreator;
20+
import com.fasterxml.jackson.annotation.JsonProperty;
21+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
22+
import com.google.adk.JsonBaseModel;
1923
import com.google.auto.value.AutoValue;
2024
import com.google.genai.types.Content;
2125
import java.util.List;
2226

2327
/** Represents an few-shot example. */
2428
@AutoValue
25-
public abstract class Example {
29+
@JsonDeserialize(builder = Example.Builder.class)
30+
public abstract class Example extends JsonBaseModel {
31+
@JsonProperty("input")
2632
public abstract Content input();
2733

34+
@JsonProperty("output")
2835
public abstract List<Content> output();
2936

3037
public static Builder builder() {
@@ -36,10 +43,17 @@ public static Builder builder() {
3643
/** Builder for constructing {@link Example} instances. */
3744
@AutoValue.Builder
3845
public abstract static class Builder {
46+
@JsonProperty("input")
3947
public abstract Builder input(Content input);
4048

49+
@JsonProperty("output")
4150
public abstract Builder output(List<Content> output);
4251

52+
@JsonCreator
53+
private static Builder create() {
54+
return builder();
55+
}
56+
4357
public abstract Example build();
4458
}
4559
}

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,13 @@
1717
package com.google.adk.tools;
1818

1919
import com.fasterxml.jackson.core.JsonProcessingException;
20+
import com.fasterxml.jackson.core.type.TypeReference;
2021
import com.google.adk.JsonBaseModel;
2122
import com.google.adk.SchemaUtils;
2223
import com.google.adk.agents.BaseAgent;
24+
import com.google.adk.agents.BaseAgentConfig;
25+
import com.google.adk.agents.ConfigAgentUtils;
26+
import com.google.adk.agents.ConfigAgentUtils.ConfigurationException;
2327
import com.google.adk.agents.LlmAgent;
2428
import com.google.adk.events.Event;
2529
import com.google.adk.runner.InMemoryRunner;
@@ -40,6 +44,24 @@ public class AgentTool extends BaseTool {
4044
private final BaseAgent agent;
4145
private final boolean skipSummarization;
4246

47+
public static BaseTool fromConfig(ToolArgsConfig args, String configAbsPath)
48+
throws ConfigurationException {
49+
var agentRef = args.getOrEmpty("agent", new TypeReference<BaseAgentConfig.AgentRefConfig>() {});
50+
if (agentRef.isEmpty()) {
51+
throw new ConfigurationException("AgentTool config requires 'agent' argument.");
52+
}
53+
54+
ImmutableList<BaseAgent> resolvedAgents =
55+
ConfigAgentUtils.resolveSubAgents(ImmutableList.of(agentRef.get()), configAbsPath);
56+
57+
if (resolvedAgents.isEmpty()) {
58+
throw new ConfigurationException("Failed to resolve agent.");
59+
}
60+
61+
BaseAgent agent = resolvedAgents.get(0);
62+
return AgentTool.create(agent, args.getOrDefault("skipSummarization", false).booleanValue());
63+
}
64+
4365
public static AgentTool create(BaseAgent agent, boolean skipSummarization) {
4466
return new AgentTool(agent, skipSummarization);
4567
}

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

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.fasterxml.jackson.annotation.JsonAnyGetter;
2222
import com.fasterxml.jackson.annotation.JsonAnySetter;
2323
import com.fasterxml.jackson.annotation.JsonIgnore;
24+
import com.fasterxml.jackson.core.type.TypeReference;
2425
import com.google.adk.JsonBaseModel;
2526
import com.google.adk.agents.ConfigAgentUtils.ConfigurationException;
2627
import com.google.adk.models.LlmRequest;
@@ -39,6 +40,8 @@
3940
import java.util.Optional;
4041
import javax.annotation.Nonnull;
4142
import org.jspecify.annotations.Nullable;
43+
import org.slf4j.Logger;
44+
import org.slf4j.LoggerFactory;
4245

4346
/** The base class for all ADK tools. */
4447
public abstract class BaseTool {
@@ -199,6 +202,8 @@ public static BaseTool fromConfig(ToolConfig config, String configAbsPath)
199202
// TODO implement this class
200203
public static class ToolArgsConfig extends JsonBaseModel {
201204

205+
private static final Logger log = LoggerFactory.getLogger(ToolArgsConfig.class);
206+
202207
@JsonIgnore private final Map<String, Object> additionalProperties = new HashMap<>();
203208

204209
public boolean isEmpty() {
@@ -215,8 +220,25 @@ public ToolArgsConfig put(String key, Object value) {
215220
return this;
216221
}
217222

218-
public Object get(String key) {
219-
return additionalProperties.get(key);
223+
public <T> Optional<T> getOrEmpty(String key, TypeReference<T> typeReference) {
224+
if (!additionalProperties.containsKey(key)) {
225+
return Optional.empty();
226+
}
227+
try {
228+
return Optional.of(
229+
JsonBaseModel.getMapper().convertValue(additionalProperties.get(key), typeReference));
230+
} catch (IllegalArgumentException e) {
231+
log.debug("Could not convert key {} into type: {}", key, e);
232+
return Optional.empty();
233+
}
234+
}
235+
236+
public <T> T getOrDefault(String key, T defaultValue) {
237+
if (!additionalProperties.containsKey(key)) {
238+
return defaultValue;
239+
}
240+
return JsonBaseModel.getMapper()
241+
.convertValue(additionalProperties.get(key), new TypeReference<T>() {});
220242
}
221243

222244
@JsonAnyGetter

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

Lines changed: 15 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,20 @@
1717
package com.google.adk.tools;
1818

1919
import static com.google.common.base.Strings.isNullOrEmpty;
20-
import static com.google.common.collect.ImmutableList.toImmutableList;
2120

22-
import com.fasterxml.jackson.databind.ObjectMapper;
23-
import com.google.adk.JsonBaseModel;
21+
import com.fasterxml.jackson.core.type.TypeReference;
2422
import com.google.adk.agents.ConfigAgentUtils.ConfigurationException;
2523
import com.google.adk.examples.BaseExampleProvider;
2624
import com.google.adk.examples.Example;
2725
import com.google.adk.examples.ExampleUtils;
2826
import com.google.adk.models.LlmRequest;
2927
import com.google.common.collect.ImmutableList;
3028
import com.google.errorprone.annotations.CanIgnoreReturnValue;
31-
import com.google.genai.types.Content;
3229
import io.reactivex.rxjava3.core.Completable;
3330
import java.lang.reflect.Field;
3431
import java.lang.reflect.Modifier;
3532
import java.util.ArrayList;
3633
import java.util.List;
37-
import java.util.Map;
3834
import java.util.Optional;
3935

4036
/**
@@ -50,8 +46,6 @@
5046
*/
5147
public final class ExampleTool extends BaseTool {
5248

53-
private static final ObjectMapper MAPPER = JsonBaseModel.getMapper();
54-
5549
private final Optional<BaseExampleProvider> exampleProvider;
5650
private final Optional<List<Example>> examples;
5751

@@ -102,52 +96,24 @@ public static ExampleTool fromConfig(ToolArgsConfig args, String configAbsPath)
10296
if (args == null || args.isEmpty()) {
10397
throw new ConfigurationException("ExampleTool requires 'examples' argument");
10498
}
105-
Object examplesArg = args.get("examples");
106-
if (examplesArg == null) {
107-
throw new ConfigurationException("ExampleTool missing 'examples' argument");
108-
}
10999

110-
try {
111-
if (examplesArg instanceof String string) {
112-
BaseExampleProvider provider = resolveExampleProvider(string);
113-
return ExampleTool.builder().setExampleProvider(provider).build();
114-
}
115-
if (examplesArg instanceof List) {
116-
@SuppressWarnings("unchecked")
117-
List<Object> rawList = (List<Object>) examplesArg;
118-
List<Example> examples = new ArrayList<>();
119-
for (Object o : rawList) {
120-
if (!(o instanceof Map)) {
121-
throw new ConfigurationException(
122-
"Invalid example entry. Expected a map with 'input' and 'output'.");
123-
}
124-
@SuppressWarnings("unchecked")
125-
Map<String, Object> m = (Map<String, Object>) o;
126-
Object inputObj = m.get("input");
127-
Object outputObj = m.get("output");
128-
if (inputObj == null || outputObj == null) {
129-
throw new ConfigurationException("Each example must include 'input' and 'output'.");
130-
}
131-
Content input = MAPPER.convertValue(inputObj, Content.class);
132-
@SuppressWarnings("unchecked")
133-
List<Object> outList = (List<Object>) outputObj;
134-
ImmutableList<Content> outputs =
135-
outList.stream()
136-
.map(e -> MAPPER.convertValue(e, Content.class))
137-
.collect(toImmutableList());
138-
examples.add(Example.builder().input(input).output(outputs).build());
139-
}
140-
Builder b = ExampleTool.builder();
141-
for (Example ex : examples) {
142-
b.addExample(ex);
143-
}
144-
return b.build();
100+
var maybeExamplesProvider = args.getOrEmpty("examples", new TypeReference<String>() {});
101+
if (maybeExamplesProvider.isPresent()) {
102+
BaseExampleProvider provider = resolveExampleProvider(maybeExamplesProvider.get());
103+
return ExampleTool.builder().setExampleProvider(provider).build();
104+
}
105+
var maybeListOfExamples = args.getOrEmpty("examples", new TypeReference<List<Example>>() {});
106+
if (maybeListOfExamples.isPresent()) {
107+
var b = ExampleTool.builder();
108+
for (Example example : maybeListOfExamples.get()) {
109+
b.addExample(example);
145110
}
146-
} catch (RuntimeException e) {
147-
throw new ConfigurationException("Failed to parse ExampleTool examples", e);
111+
return b.build();
148112
}
113+
149114
throw new ConfigurationException(
150-
"Unsupported 'examples' type. Provide a string provider ref or list of examples.");
115+
"ExampleTool requires 'examples' argument to be either an example provider name (String) or"
116+
+ " a list of examples (List<Example>).");
151117
}
152118

153119
/** Overload to match resolver which passes only ToolArgsConfig. */

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ public void testResolveToolFromClass_withFromConfigMethod() throws Exception {
215215
BaseTool.ToolArgsConfig args =
216216
new BaseTool.ToolArgsConfig().put("key1", "value1").put("key2", 42).put("key3", true);
217217

218-
BaseTool resolved = ToolResolver.resolveToolFromClass(className, args);
218+
BaseTool resolved = ToolResolver.resolveToolFromClass(className, args, "/unused/config.yaml");
219219

220220
assertThat(resolved).isNotNull();
221221
assertThat(resolved).isInstanceOf(TestToolWithFromConfig.class);
@@ -226,7 +226,7 @@ public void testResolveToolFromClass_withDefaultConstructor() throws Exception {
226226
String className = TestToolWithDefaultConstructor.class.getName();
227227

228228
// Test resolving tool from class with default constructor (no args)
229-
BaseTool resolved = ToolResolver.resolveToolFromClass(className, null);
229+
BaseTool resolved = ToolResolver.resolveToolFromClass(className, null, "/unused/config.yaml");
230230

231231
assertThat(resolved).isNotNull();
232232
assertThat(resolved).isInstanceOf(TestToolWithDefaultConstructor.class);
@@ -239,7 +239,8 @@ public void testResolveToolFromClass_missingFromConfigWithArgs() {
239239

240240
ConfigurationException exception =
241241
assertThrows(
242-
ConfigurationException.class, () -> ToolResolver.resolveToolFromClass(className, args));
242+
ConfigurationException.class,
243+
() -> ToolResolver.resolveToolFromClass(className, args, "/unused/config.yaml"));
243244

244245
assertThat(exception)
245246
.hasMessageThat()
@@ -252,7 +253,8 @@ public void testResolveToolFromClass_missingDefaultConstructor() {
252253

253254
ConfigurationException exception =
254255
assertThrows(
255-
ConfigurationException.class, () -> ToolResolver.resolveToolFromClass(className, null));
256+
ConfigurationException.class,
257+
() -> ToolResolver.resolveToolFromClass(className, null, "/unused/config.yaml"));
256258

257259
assertThat(exception).hasMessageThat().contains("does not have a default constructor");
258260
}
@@ -366,7 +368,8 @@ private TestToolWithFromConfig(String param) {
366368
super("test_tool_from_config", "Test tool from config: " + param);
367369
}
368370

369-
public static TestToolWithFromConfig fromConfig(BaseTool.ToolArgsConfig args) {
371+
public static TestToolWithFromConfig fromConfig(
372+
BaseTool.ToolArgsConfig args, String configAbsPath) {
370373
return new TestToolWithFromConfig("test_param");
371374
}
372375

0 commit comments

Comments
 (0)