Skip to content

Commit 5ee51fd

Browse files
google-genai-botcopybara-github
authored andcommitted
feat: Add tools and toolset to use SkillSource in ADK agents
This change introduces a set of tools and a toolset to enable ADK agents to interact with and use skills loaded via `SkillSource`. Key changes: - ListSkillsTool: Lists available skills. - LoadSkillTool: Loads skill instructions. - LoadSkillResourceTool: Loads skill resources. - SkillToolset: Groups the above tools. - Integrates SkillSource into LlmAgent and BaseLlmFlow. - Tests for all new tools. PiperOrigin-RevId: 919952737
1 parent 5bad20a commit 5ee51fd

18 files changed

Lines changed: 37 additions & 1676 deletions

core/src/main/java/com/google/adk/flows/llmflows/BaseLlmFlow.java

Lines changed: 13 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,7 @@
3838
import com.google.adk.models.LlmRequest;
3939
import com.google.adk.models.LlmResponse;
4040
import com.google.adk.telemetry.Tracing;
41-
import com.google.adk.tools.BaseTool;
42-
import com.google.adk.tools.BaseToolset;
4341
import com.google.adk.tools.ToolContext;
44-
import com.google.common.annotations.VisibleForTesting;
4542
import com.google.common.collect.ImmutableList;
4643
import com.google.common.collect.Iterables;
4744
import com.google.genai.types.FunctionResponse;
@@ -61,7 +58,6 @@
6158
import java.util.Optional;
6259
import java.util.Set;
6360
import java.util.concurrent.atomic.AtomicReference;
64-
import java.util.function.BiFunction;
6561
import org.slf4j.Logger;
6662
import org.slf4j.LoggerFactory;
6763

@@ -100,8 +96,20 @@ private Flowable<Event> preprocess(
10096
Context currentContext = Context.current();
10197
LlmAgent agent = (LlmAgent) context.agent();
10298

99+
RequestProcessor toolsProcessor =
100+
(ctx, req) -> {
101+
LlmRequest.Builder builder = req.toBuilder();
102+
return agent
103+
.canonicalTools(new ReadonlyContext(ctx))
104+
.concatMapCompletable(
105+
tool -> tool.processLlmRequest(builder, ToolContext.builder(ctx).build()))
106+
.andThen(
107+
Single.fromCallable(
108+
() -> RequestProcessingResult.create(builder.build(), ImmutableList.of())));
109+
};
110+
103111
Iterable<RequestProcessor> allProcessors =
104-
Iterables.concat(requestProcessors, ImmutableList.of(getRequestProcessorFromTools(agent)));
112+
Iterables.concat(requestProcessors, ImmutableList.of(toolsProcessor));
105113

106114
return Flowable.fromIterable(allProcessors)
107115
.concatMap(
@@ -113,48 +121,6 @@ private Flowable<Event> preprocess(
113121
result -> result.events() != null ? result.events() : ImmutableList.of()));
114122
}
115123

116-
/**
117-
* Constructs a {@link RequestProcessor} that sequentially applies the {@code processLlmRequest}
118-
* methods of all tools and toolsets associated with this agent to the incoming {@link
119-
* LlmRequest}.
120-
*
121-
* @return A {@link RequestProcessor} that applies tool-specific modifications to LLM requests.
122-
*/
123-
@VisibleForTesting
124-
RequestProcessor getRequestProcessorFromTools(LlmAgent agent) {
125-
return (context, request) -> {
126-
ReadonlyContext readonlyContext = new ReadonlyContext(context);
127-
List<BiFunction<LlmRequest.Builder, ToolContext, Completable>> processors = new ArrayList<>();
128-
129-
for (Object toolOrToolset : agent.toolsUnion()) {
130-
if (toolOrToolset instanceof BaseTool baseTool) {
131-
processors.add(baseTool::processLlmRequest);
132-
} else if (toolOrToolset instanceof BaseToolset baseToolset) {
133-
// First apply the toolset's own request processor, then unwrap all tools from the toolset
134-
// and apply each individual tool's request processor sequentially.
135-
processors.add(
136-
(builder, ctx) ->
137-
baseToolset
138-
.processLlmRequest(builder, ctx)
139-
.andThen(baseToolset.getTools(readonlyContext))
140-
.concatMapCompletable(b -> b.processLlmRequest(builder, ctx)));
141-
} else {
142-
throw new IllegalArgumentException(
143-
"Object in tools list is not of a supported type: "
144-
+ toolOrToolset.getClass().getName());
145-
}
146-
}
147-
148-
LlmRequest.Builder builder = request.toBuilder();
149-
ToolContext toolContext = ToolContext.builder(context).build();
150-
return Flowable.fromIterable(processors)
151-
.concatMapCompletable(f -> f.apply(builder, toolContext))
152-
.andThen(
153-
Single.fromCallable(
154-
() -> RequestProcessingResult.create(builder.build(), ImmutableList.of())));
155-
};
156-
}
157-
158124
/**
159125
* Post-processes the LLM response after receiving it from the LLM. Executes all registered {@link
160126
* ResponseProcessor} instances. Emits events for the model response and any subsequent function

core/src/main/java/com/google/adk/skills/AbstractSkillSource.java

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616

1717
package com.google.adk.skills;
1818

19-
import static com.google.adk.skills.SkillSourceException.SKILL_FORMAT_ERROR;
20-
import static com.google.adk.skills.SkillSourceException.SKILL_LOAD_ERROR;
2119
import static java.nio.channels.Channels.newReader;
2220
import static java.nio.charset.StandardCharsets.UTF_8;
2321

@@ -84,14 +82,12 @@ private Frontmatter loadFrontmatter(String skillName, PathT skillMdPath)
8482
Frontmatter frontmatter = yamlMapper.readValue(yaml, Frontmatter.class);
8583
if (!frontmatter.name().equals(skillName)) {
8684
throw new SkillSourceException(
87-
"Skill name in the frontmatter '%s' does not match skill name '%s'."
88-
.formatted(frontmatter.name(), skillName),
89-
SKILL_LOAD_ERROR);
85+
"Skill name '%s' does not match directory name '%s'."
86+
.formatted(frontmatter.name(), skillName));
9087
}
9188
return frontmatter;
9289
} catch (IOException e) {
93-
throw new SkillSourceException(
94-
"Cannot load frontmatter for skill '" + skillName + "'", SKILL_LOAD_ERROR, e);
90+
throw new SkillSourceException("Cannot load frontmatter for skill '" + skillName + "'", e);
9591
}
9692
}
9793

@@ -104,9 +100,7 @@ public Single<String> loadInstructions(String skillName) {
104100
return readInstructions(reader);
105101
} catch (IOException e) {
106102
throw new SkillSourceException(
107-
"Failed to load instruction for skill '" + skillName + "'",
108-
SKILL_LOAD_ERROR,
109-
e);
103+
"Failed to load instruction for skill '" + skillName + "'", e);
110104
}
111105
});
112106
}
@@ -146,8 +140,7 @@ private String readFrontmatterYaml(BufferedReader reader)
146140
throws IOException, SkillSourceException {
147141
String line = reader.readLine();
148142
if (line == null || !line.trim().equals(THREE_DASHES)) {
149-
throw new SkillSourceException(
150-
"Skill file must start with " + THREE_DASHES, SKILL_FORMAT_ERROR);
143+
throw new SkillSourceException("Skill file must start with " + THREE_DASHES);
151144
}
152145

153146
StringBuilder sb = new StringBuilder();
@@ -158,15 +151,14 @@ private String readFrontmatterYaml(BufferedReader reader)
158151
sb.append(line).append("\n");
159152
}
160153
throw new SkillSourceException(
161-
"Skill file frontmatter not properly closed with " + THREE_DASHES, SKILL_FORMAT_ERROR);
154+
"Skill file frontmatter not properly closed with " + THREE_DASHES);
162155
}
163156

164157
private String readInstructions(BufferedReader reader) throws IOException, SkillSourceException {
165158
// Skip the frontmatter block
166159
String line = reader.readLine();
167160
if (line == null || !line.trim().equals(THREE_DASHES)) {
168-
throw new SkillSourceException(
169-
"Skill file must start with " + THREE_DASHES, SKILL_FORMAT_ERROR);
161+
throw new SkillSourceException("Skill file must start with " + THREE_DASHES);
170162
}
171163
boolean dashClosed = false;
172164
while ((line = reader.readLine()) != null) {
@@ -177,7 +169,7 @@ private String readInstructions(BufferedReader reader) throws IOException, Skill
177169
}
178170
if (!dashClosed) {
179171
throw new SkillSourceException(
180-
"Skill file frontmatter not properly closed with " + THREE_DASHES, SKILL_FORMAT_ERROR);
172+
"Skill file frontmatter not properly closed with " + THREE_DASHES);
181173
}
182174
// Read the instructions till the end of the file
183175
StringBuilder sb = new StringBuilder();

core/src/main/java/com/google/adk/skills/InMemorySkillSource.java

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616

1717
package com.google.adk.skills;
1818

19-
import static com.google.adk.skills.SkillSourceException.RESOURCE_NOT_FOUND;
20-
import static com.google.adk.skills.SkillSourceException.SKILL_NOT_FOUND;
2119
import static com.google.common.base.Preconditions.checkState;
2220
import static com.google.common.collect.ImmutableList.toImmutableList;
2321
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -58,8 +56,7 @@ public Single<ImmutableMap<String, Frontmatter>> listFrontmatters() {
5856
public Single<ImmutableList<String>> listResources(String skillName, String resourceDirectory) {
5957
SkillData data = skills.get(skillName);
6058
if (data == null) {
61-
return Single.error(
62-
new SkillSourceException("Skill not found: " + skillName, SKILL_NOT_FOUND));
59+
return Single.error(new SkillSourceException("Skill not found: " + skillName));
6360
}
6461
String prefix =
6562
resourceDirectory.isEmpty()
@@ -70,8 +67,7 @@ public Single<ImmutableList<String>> listResources(String skillName, String reso
7067
&& data.resources().keySet().stream().noneMatch(path -> path.startsWith(prefix))) {
7168
return Single.error(
7269
new SkillSourceException(
73-
"Resource directory not found: " + resourceDirectory + " for skill: " + skillName,
74-
RESOURCE_NOT_FOUND));
70+
"Resource directory not found: " + resourceDirectory + " for skill: " + skillName));
7571
}
7672

7773
return Single.just(
@@ -96,16 +92,13 @@ public Single<ByteSource> loadResource(String skillName, String resourcePath) {
9692
.map(SkillData::resources)
9793
.mapOptional(m -> Optional.ofNullable(m.get(resourcePath)))
9894
.switchIfEmpty(
99-
Single.error(
100-
new SkillSourceException(
101-
"Resource not found: " + resourcePath, RESOURCE_NOT_FOUND)));
95+
Single.error(new SkillSourceException("Resource not found: " + resourcePath)));
10296
}
10397

10498
private Single<SkillData> getSkillData(String skillName) {
10599
SkillData data = skills.get(skillName);
106100
if (data == null) {
107-
return Single.error(
108-
new SkillSourceException("Skill not found: " + skillName, SKILL_NOT_FOUND));
101+
return Single.error(new SkillSourceException("Skill not found: " + skillName));
109102
}
110103
return Single.just(data);
111104
}

core/src/main/java/com/google/adk/skills/LocalSkillSource.java

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,6 @@
1616

1717
package com.google.adk.skills;
1818

19-
import static com.google.adk.skills.SkillSourceException.RESOURCE_LOAD_ERROR;
20-
import static com.google.adk.skills.SkillSourceException.RESOURCE_NOT_FOUND;
21-
import static com.google.adk.skills.SkillSourceException.SKILL_LOAD_ERROR;
22-
import static com.google.adk.skills.SkillSourceException.SKILL_NOT_FOUND;
2319
import static com.google.common.collect.ImmutableList.toImmutableList;
2420
import static java.nio.file.Files.isDirectory;
2521

@@ -47,16 +43,14 @@ public LocalSkillSource(Path skillsBasePath) {
4743
public Single<ImmutableList<String>> listResources(String skillName, String resourceDirectory) {
4844
Path skillDir = skillsBasePath.resolve(skillName);
4945
if (!isDirectory(skillDir)) {
50-
return Single.error(
51-
new SkillSourceException("Skill not found: " + skillName, SKILL_NOT_FOUND));
46+
return Single.error(new SkillSourceException("Skill not found: " + skillName));
5247
}
5348
Path resourceDir = skillDir.resolve(resourceDirectory);
5449
if (!isDirectory(resourceDir)) {
5550
return Single.error(
5651
new SkillSourceException(
5752
"Resource directory '%s' not found for skill '%s'"
58-
.formatted(resourceDirectory, skillName),
59-
RESOURCE_NOT_FOUND));
53+
.formatted(resourceDirectory, skillName)));
6054
}
6155

6256
return Single.fromCallable(
@@ -73,9 +67,7 @@ public Single<ImmutableList<String>> listResources(String skillName, String reso
7367
t ->
7468
Single.error(
7569
new SkillSourceException(
76-
"Failed to traverse resource directory: " + resourceDirectory,
77-
RESOURCE_LOAD_ERROR,
78-
t)));
70+
"Failed to traverse resource directory: " + resourceDirectory, t)));
7971
}
8072

8173
@Override
@@ -86,9 +78,7 @@ protected Flowable<SkillMdPath> listSkills() {
8678
t ->
8779
Flowable.error(
8880
new SkillSourceException(
89-
"Failed to list skills in directory: " + skillsBasePath,
90-
SKILL_LOAD_ERROR,
91-
t)))
81+
"Failed to list skills in directory: " + skillsBasePath, t)))
9282
.filter(Files::isDirectory)
9383
.mapOptional(this::findSkillMd)
9484
.map(skillMd -> new SkillMdPath(skillMd.getParent().getFileName().toString(), skillMd));
@@ -98,8 +88,7 @@ protected Flowable<SkillMdPath> listSkills() {
9888
protected Single<Path> findResourcePath(String skillName, String resourcePath) {
9989
Path file = skillsBasePath.resolve(skillName).resolve(resourcePath);
10090
if (!Files.exists(file)) {
101-
return Single.error(
102-
new SkillSourceException("Resource not found: " + file, RESOURCE_NOT_FOUND));
91+
return Single.error(new SkillSourceException("Resource not found: " + file));
10392
}
10493
return Single.just(file);
10594
}
@@ -108,13 +97,11 @@ protected Single<Path> findResourcePath(String skillName, String resourcePath) {
10897
protected Single<Path> findSkillMdPath(String skillName) {
10998
Path skillDir = skillsBasePath.resolve(skillName);
11099
if (!isDirectory(skillDir)) {
111-
return Single.error(
112-
new SkillSourceException("Skill directory not found: " + skillName, SKILL_NOT_FOUND));
100+
return Single.error(new SkillSourceException("Skill directory not found: " + skillName));
113101
}
114102
return Maybe.fromOptional(findSkillMd(skillDir))
115103
.switchIfEmpty(
116-
Single.error(
117-
new SkillSourceException("SKILL.md not found in " + skillName, SKILL_NOT_FOUND)));
104+
Single.error(new SkillSourceException("SKILL.md not found in " + skillName)));
118105
}
119106

120107
@Override

core/src/main/java/com/google/adk/skills/SkillSourceException.java

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -22,43 +22,11 @@
2222
*/
2323
public final class SkillSourceException extends Exception {
2424

25-
public static final String SKILL_LOAD_ERROR = "SKILL_LOAD_ERROR";
26-
public static final String SKILL_NOT_FOUND = "SKILL_NOT_FOUND";
27-
public static final String SKILL_FORMAT_ERROR = "SKILL_FORMAT_ERROR";
28-
public static final String RESOURCE_LOAD_ERROR = "RESOURCE_LOAD_ERROR";
29-
public static final String RESOURCE_NOT_FOUND = "RESOURCE_NOT_FOUND";
30-
31-
private final String errorCode;
32-
33-
/**
34-
* Constructs a new exception with the specified detail message and error code.
35-
*
36-
* @param message The detail message.
37-
* @param errorCode The specific error code categorizing the failure.
38-
*/
39-
public SkillSourceException(String message, String errorCode) {
25+
public SkillSourceException(String message) {
4026
super(message);
41-
this.errorCode = errorCode;
4227
}
4328

44-
/**
45-
* Constructs a new exception with the specified detail message, error code, and cause.
46-
*
47-
* @param message The detail message.
48-
* @param errorCode The specific error code categorizing the failure.
49-
* @param cause The cause.
50-
*/
51-
public SkillSourceException(String message, String errorCode, Throwable cause) {
29+
public SkillSourceException(String message, Throwable cause) {
5230
super(message, cause);
53-
this.errorCode = errorCode;
54-
}
55-
56-
/**
57-
* Returns the error code categorizing the failure.
58-
*
59-
* @return The error code string.
60-
*/
61-
public String getErrorCode() {
62-
return errorCode;
6331
}
6432
}

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

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,18 @@
1717
package com.google.adk.tools;
1818

1919
import com.google.adk.agents.ReadonlyContext;
20-
import com.google.adk.models.LlmRequest;
21-
import io.reactivex.rxjava3.core.Completable;
2220
import io.reactivex.rxjava3.core.Flowable;
2321
import java.util.List;
2422
import org.jspecify.annotations.Nullable;
2523

2624
/** Base interface for toolsets. */
2725
public interface BaseToolset extends AutoCloseable {
2826

29-
/** Processes the outgoing {@link LlmRequest.Builder}. */
30-
default Completable processLlmRequest(
31-
LlmRequest.Builder llmRequestBuilder, ToolContext toolContext) {
32-
return Completable.complete();
33-
}
34-
3527
/**
3628
* Return all tools in the toolset based on the provided context.
3729
*
3830
* @param readonlyContext Context used to filter tools available to the agent.
39-
* @return A Flowable emitting tools available under the specified context.
31+
* @return A Single emitting a list of tools available under the specified context.
4032
*/
4133
Flowable<BaseTool> getTools(ReadonlyContext readonlyContext);
4234

0 commit comments

Comments
 (0)