Skip to content

Commit 0d6e63d

Browse files
google-genai-botcopybara-github
authored andcommitted
feat: Add BaseToolset and update McpToolset to use the new interface
PiperOrigin-RevId: 781532922
1 parent a211ac4 commit 0d6e63d

File tree

6 files changed

+88
-87
lines changed

6 files changed

+88
-87
lines changed

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

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

1717
package com.google.adk.agents;
1818

19-
import static com.google.common.collect.ImmutableList.toImmutableList;
2019
import static java.util.stream.Collectors.joining;
2120

2221
import com.fasterxml.jackson.core.JsonProcessingException;
@@ -698,31 +697,29 @@ public Single<String> canonicalGlobalInstruction(ReadonlyContext context) {
698697
* @param context The context to retrieve the session state.
699698
* @return The resolved list of tools as a {@link Single} wrapped list of {@link BaseTool}.
700699
*/
701-
public Single<List<BaseTool>> canonicalTools(Optional<ReadonlyContext> context) {
702-
List<Single<List<BaseTool>>> toolSingles = new ArrayList<>();
700+
public Flowable<BaseTool> canonicalTools(Optional<ReadonlyContext> context) {
701+
List<Flowable<BaseTool>> toolFlowables = new ArrayList<>();
703702
for (Object toolOrToolset : toolsUnion) {
704703
if (toolOrToolset instanceof BaseTool baseTool) {
705-
toolSingles.add(Single.just(ImmutableList.of(baseTool)));
704+
toolFlowables.add(Flowable.just(baseTool));
706705
} else if (toolOrToolset instanceof BaseToolset baseToolset) {
707-
toolSingles.add(baseToolset.getTools(context.orElse(null)));
706+
toolFlowables.add(baseToolset.getTools(context.orElse(null)));
708707
} else {
709708
throw new IllegalArgumentException(
710709
"Object in tools list is not of a supported type: "
711710
+ toolOrToolset.getClass().getName());
712711
}
713712
}
714-
return Single.concat(toolSingles)
715-
.toList()
716-
.map(listOfLists -> listOfLists.stream().flatMap(List::stream).collect(toImmutableList()));
713+
return Flowable.concat(toolFlowables);
717714
}
718715

719716
/** Overload of canonicalTools that defaults to an empty context. */
720-
public Single<List<BaseTool>> canonicalTools() {
717+
public Flowable<BaseTool> canonicalTools() {
721718
return canonicalTools(Optional.empty());
722719
}
723720

724721
/** Convenience overload of canonicalTools that accepts a non-optional ReadonlyContext. */
725-
public Single<List<BaseTool>> canonicalTools(ReadonlyContext context) {
722+
public Flowable<BaseTool> canonicalTools(ReadonlyContext context) {
726723
return canonicalTools(Optional.ofNullable(context));
727724
}
728725

@@ -758,7 +755,11 @@ public IncludeContents includeContents() {
758755
return includeContents;
759756
}
760757

761-
public List<Object> tools() {
758+
public List<BaseTool> tools() {
759+
return canonicalTools().toList().blockingGet();
760+
}
761+
762+
public List<Object> toolsUnion() {
762763
return toolsUnion;
763764
}
764765

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

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,10 @@ protected Single<RequestProcessingResult> preprocess(
115115

116116
return agent
117117
.canonicalTools(new ReadonlyContext(context))
118-
.flatMapCompletable(
119-
tools ->
120-
Flowable.fromIterable(tools)
121-
.concatMapCompletable(
122-
tool ->
123-
tool.processLlmRequest(
124-
updatedRequestBuilder, ToolContext.builder(context).build())))
118+
.concatMapCompletable(
119+
tool ->
120+
tool.processLlmRequest(
121+
updatedRequestBuilder, ToolContext.builder(context).build()))
125122
.andThen(
126123
Single.fromCallable(
127124
() -> {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.google.adk.tools;
22

33
import com.google.adk.agents.ReadonlyContext;
4-
import io.reactivex.rxjava3.core.Single;
4+
import io.reactivex.rxjava3.core.Flowable;
55
import java.util.List;
66
import java.util.Optional;
77

@@ -14,7 +14,7 @@ public interface BaseToolset extends AutoCloseable {
1414
* @param readonlyContext Context used to filter tools available to the agent.
1515
* @return A Single emitting a list of tools available under the specified context.
1616
*/
17-
Single<List<BaseTool>> getTools(ReadonlyContext readonlyContext);
17+
Flowable<BaseTool> getTools(ReadonlyContext readonlyContext);
1818

1919
/**
2020
* Performs cleanup and releases resources held by the toolset.

core/src/main/java/com/google/adk/tools/mcp/McpToolset.java

Lines changed: 65 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@
2626
import io.modelcontextprotocol.client.McpSyncClient;
2727
import io.modelcontextprotocol.client.transport.ServerParameters;
2828
import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;
29-
import io.reactivex.rxjava3.core.Single;
30-
import java.util.List;
29+
import io.reactivex.rxjava3.core.Flowable;
3130
import java.util.Objects;
3231
import java.util.Optional;
3332
import org.slf4j.Logger;
@@ -151,69 +150,74 @@ public McpToolset(ServerParameters connectionParams) {
151150
}
152151

153152
@Override
154-
public Single<List<BaseTool>> getTools(ReadonlyContext readonlyContext) {
155-
return Single.fromCallable(
156-
() -> {
157-
for (int i = 0; i < MAX_RETRIES; i++) {
158-
try {
159-
if (this.mcpSession == null) {
160-
logger.info("MCP session is null or closed, initializing (attempt {}).", i + 1);
161-
this.mcpSession = this.mcpSessionManager.createSession();
162-
}
163-
164-
ListToolsResult toolsResponse = this.mcpSession.listTools();
165-
return toolsResponse.tools().stream()
166-
.map(
167-
tool ->
168-
new McpTool(
169-
tool, this.mcpSession, this.mcpSessionManager, this.objectMapper))
170-
.filter(
171-
tool ->
172-
isToolSelected(tool, toolFilter, Optional.ofNullable(readonlyContext)))
173-
.collect(toImmutableList());
174-
} catch (IllegalArgumentException e) {
175-
// This could happen if parameters for tool loading are somehow invalid.
176-
// This is likely a fatal error and should not be retried.
177-
logger.error("Invalid argument encountered during tool loading.", e);
178-
throw new McpToolLoadingException(
179-
"Invalid argument encountered during tool loading.", e);
180-
} catch (RuntimeException e) { // Catch any other unexpected runtime exceptions
181-
logger.error("Unexpected error during tool loading, retry attempt " + (i + 1), e);
182-
if (i < MAX_RETRIES - 1) {
183-
// For other general exceptions, we might still want to retry if they are
184-
// potentially transient, or if we don't have more specific handling. But it's
185-
// better to be specific. For now, we'll treat them as potentially retryable but log
186-
// them at a higher level.
153+
public Flowable<BaseTool> getTools(ReadonlyContext readonlyContext) {
154+
return Flowable.fromCallable(
155+
() -> {
156+
for (int i = 0; i < MAX_RETRIES; i++) {
187157
try {
188-
logger.info("Reinitializing MCP session before next retry for unexpected error.");
189-
this.mcpSession = this.mcpSessionManager.createSession();
190-
Thread.sleep(RETRY_DELAY_MILLIS);
191-
} catch (InterruptedException ie) {
192-
Thread.currentThread().interrupt();
193-
logger.error(
194-
"Interrupted during retry delay for loadTools (unexpected error).", ie);
158+
if (this.mcpSession == null) {
159+
logger.info("MCP session is null or closed, initializing (attempt {}).", i + 1);
160+
this.mcpSession = this.mcpSessionManager.createSession();
161+
}
162+
163+
ListToolsResult toolsResponse = this.mcpSession.listTools();
164+
return toolsResponse.tools().stream()
165+
.map(
166+
tool ->
167+
new McpTool(
168+
tool, this.mcpSession, this.mcpSessionManager, this.objectMapper))
169+
.filter(
170+
tool ->
171+
isToolSelected(
172+
tool, toolFilter, Optional.ofNullable(readonlyContext)))
173+
.collect(toImmutableList());
174+
} catch (IllegalArgumentException e) {
175+
// This could happen if parameters for tool loading are somehow invalid.
176+
// This is likely a fatal error and should not be retried.
177+
logger.error("Invalid argument encountered during tool loading.", e);
195178
throw new McpToolLoadingException(
196-
"Interrupted during retry delay (unexpected error)", ie);
197-
} catch (RuntimeException reinitE) {
198-
logger.error(
199-
"Failed to reinitialize session during retry (unexpected error).", reinitE);
200-
throw new McpInitializationException(
201-
"Failed to reinitialize session during tool loading retry (unexpected"
202-
+ " error).",
203-
reinitE);
179+
"Invalid argument encountered during tool loading.", e);
180+
} catch (RuntimeException e) { // Catch any other unexpected runtime exceptions
181+
logger.error("Unexpected error during tool loading, retry attempt " + (i + 1), e);
182+
if (i < MAX_RETRIES - 1) {
183+
// For other general exceptions, we might still want to retry if they are
184+
// potentially transient, or if we don't have more specific handling. But it's
185+
// better to be specific. For now, we'll treat them as potentially retryable but
186+
// log
187+
// them at a higher level.
188+
try {
189+
logger.info(
190+
"Reinitializing MCP session before next retry for unexpected error.");
191+
this.mcpSession = this.mcpSessionManager.createSession();
192+
Thread.sleep(RETRY_DELAY_MILLIS);
193+
} catch (InterruptedException ie) {
194+
Thread.currentThread().interrupt();
195+
logger.error(
196+
"Interrupted during retry delay for loadTools (unexpected error).", ie);
197+
throw new McpToolLoadingException(
198+
"Interrupted during retry delay (unexpected error)", ie);
199+
} catch (RuntimeException reinitE) {
200+
logger.error(
201+
"Failed to reinitialize session during retry (unexpected error).",
202+
reinitE);
203+
throw new McpInitializationException(
204+
"Failed to reinitialize session during tool loading retry (unexpected"
205+
+ " error).",
206+
reinitE);
207+
}
208+
} else {
209+
logger.error(
210+
"Failed to load tools after multiple retries due to unexpected error.", e);
211+
throw new McpToolLoadingException(
212+
"Failed to load tools after multiple retries due to unexpected error.", e);
213+
}
204214
}
205-
} else {
206-
logger.error(
207-
"Failed to load tools after multiple retries due to unexpected error.", e);
208-
throw new McpToolLoadingException(
209-
"Failed to load tools after multiple retries due to unexpected error.", e);
210215
}
211-
}
212-
}
213-
// This line should ideally not be reached if retries are handled correctly or an
214-
// exception is always thrown.
215-
throw new IllegalStateException("Unexpected state in getTools retry loop");
216-
});
216+
// This line should ideally not be reached if retries are handled correctly or an
217+
// exception is always thrown.
218+
throw new IllegalStateException("Unexpected state in getTools retry loop");
219+
})
220+
.flatMapIterable(tools -> tools);
217221
}
218222

219223
@Override

core/src/test/java/com/google/adk/tools/BaseToolsetTest.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
import static org.mockito.Mockito.mock;
55

66
import com.google.adk.agents.ReadonlyContext;
7-
import com.google.common.collect.ImmutableList;
8-
import io.reactivex.rxjava3.core.Single;
7+
import io.reactivex.rxjava3.core.Flowable;
98
import java.util.List;
109
import org.junit.Test;
1110
import org.junit.runner.RunWith;
@@ -23,15 +22,15 @@ public void testGetTools() {
2322
BaseToolset toolset =
2423
new BaseToolset() {
2524
@Override
26-
public Single<List<BaseTool>> getTools(ReadonlyContext readonlyContext) {
27-
return Single.just(ImmutableList.of(mockTool1, mockTool2));
25+
public Flowable<BaseTool> getTools(ReadonlyContext readonlyContext) {
26+
return Flowable.just(mockTool1, mockTool2);
2827
}
2928

3029
@Override
3130
public void close() throws Exception {}
3231
};
3332

34-
List<BaseTool> tools = toolset.getTools(mockContext).blockingGet();
33+
List<BaseTool> tools = toolset.getTools(mockContext).toList().blockingGet();
3534
assertThat(tools).containsExactly(mockTool1, mockTool2);
3635
}
3736
}

dev/src/main/java/com/google/adk/web/AgentGraphGenerator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ private static void buildGraphRecursive(
105105

106106
if (agent instanceof LlmAgent) {
107107
LlmAgent llmAgent = (LlmAgent) agent;
108-
List<BaseTool> tools = llmAgent.canonicalTools().blockingGet();
108+
List<BaseTool> tools = llmAgent.canonicalTools().toList().blockingGet();
109109
if (tools != null) {
110110
for (BaseTool tool : tools) {
111111
if (tool != null) {

0 commit comments

Comments
 (0)