Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -586,8 +586,7 @@ private static Maybe<Map<String, Object>> maybeInvokeAfterToolCall(

private static Maybe<Map<String, Object>> callTool(
BaseTool tool, Map<String, Object> args, ToolContext toolContext, Context parentContext) {
return tool.runAsync(args, toolContext)
.toMaybe()
return tool.runMaybeAsync(args, toolContext)
.doOnSubscribe(
d ->
Tracing.traceToolCall(
Expand Down
32 changes: 32 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 @@ -34,6 +34,7 @@
import com.google.genai.types.LiveConnectConfig;
import com.google.genai.types.Tool;
import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.rxjava3.core.Single;
import java.util.HashMap;
import java.util.Map;
Expand All @@ -48,6 +49,8 @@ public abstract class BaseTool {
private final String name;
private final String description;
private final boolean isLongRunning;
private final boolean overridesRunAsync;
private final boolean overridesRunMaybeAsync;
private final HashMap<String, Object> customMetadata;

protected BaseTool(@Nonnull String name, @Nonnull String description) {
Expand All @@ -58,6 +61,8 @@ protected BaseTool(@Nonnull String name, @Nonnull String description, boolean is
this.name = name;
this.description = description;
this.isLongRunning = isLongRunning;
overridesRunAsync = overridesMethod("runAsync");
overridesRunMaybeAsync = overridesMethod("runMaybeAsync");
customMetadata = new HashMap<>();
}

Expand Down Expand Up @@ -90,6 +95,24 @@ public void setCustomMetadata(String key, Object value) {

/** Calls a tool. */
public Single<Map<String, Object>> runAsync(Map<String, Object> args, ToolContext toolContext) {
if (overridesRunMaybeAsync) {
return runMaybeAsync(args, toolContext).defaultIfEmpty(ImmutableMap.<String, Object>of());
}
throw new UnsupportedOperationException("This method is not implemented.");
}

/**
* Calls a tool and optionally returns a function response.
*
* <p>Override this method for long-running tools that may end the current invocation without
* emitting a function response event. This default implementation delegates to {@link
* #runAsync(Map, ToolContext)} for backwards compatibility.
*/
public Maybe<Map<String, Object>> runMaybeAsync(
Map<String, Object> args, ToolContext toolContext) {
if (overridesRunAsync && !overridesRunMaybeAsync) {
return runAsync(args, toolContext).toMaybe();
}
throw new UnsupportedOperationException("This method is not implemented.");
}

Expand Down Expand Up @@ -180,6 +203,15 @@ private static ImmutableList<Tool> findToolsWithoutFunctionDeclarations(LlmReque
.orElse(ImmutableList.of());
}

private boolean overridesMethod(String methodName) {
try {
return getClass().getMethod(methodName, Map.class, ToolContext.class).getDeclaringClass()
!= BaseTool.class;
} catch (NoSuchMethodException e) {
throw new IllegalStateException("Missing tool method: " + methodName, e);
}
}

/**
* Creates a tool instance from a config.
*
Expand Down
17 changes: 13 additions & 4 deletions core/src/main/java/com/google/adk/tools/FunctionTool.java
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,12 @@ public boolean isStreaming() {

@Override
public Single<Map<String, Object>> runAsync(Map<String, Object> args, ToolContext toolContext) {
return runMaybeAsync(args, toolContext).defaultIfEmpty(ImmutableMap.<String, Object>of());
}

@Override
public Maybe<Map<String, Object>> runMaybeAsync(
Map<String, Object> args, ToolContext toolContext) {
try {
if (requireConfirmation) {
if (toolContext.toolConfirmation().isEmpty()) {
Expand All @@ -253,17 +259,20 @@ public Single<Map<String, Object>> runAsync(Map<String, Object> args, ToolContex
"Please approve or reject the tool call %s() by responding with a"
+ " FunctionResponse with an expected ToolConfirmation payload.",
name()));
return Single.just(
return Maybe.just(
ImmutableMap.of(
"error", "This tool call requires confirmation, please approve or reject."));
} else if (!toolContext.toolConfirmation().get().confirmed()) {
return Single.just(ImmutableMap.of("error", "This tool call is rejected."));
return Maybe.just(ImmutableMap.of("error", "This tool call is rejected."));
}
}
return this.call(args, toolContext).defaultIfEmpty(ImmutableMap.of());
Maybe<Map<String, Object>> functionResult = this.call(args, toolContext);
return longRunning()
? functionResult
: functionResult.switchIfEmpty(Maybe.just(ImmutableMap.<String, Object>of()));
} catch (Exception e) {
logger.error("Exception occurred while calling function tool: " + func.getName(), e);
return Single.just(
return Maybe.just(
ImmutableMap.of("status", "error", "message", "An internal error occurred."));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,16 @@
import com.google.adk.agents.RunConfig.ToolExecutionMode;
import com.google.adk.events.Event;
import com.google.adk.testing.TestUtils;
import com.google.adk.tools.BaseTool;
import com.google.adk.tools.ToolContext;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.genai.types.Content;
import com.google.genai.types.FunctionCall;
import com.google.genai.types.FunctionResponse;
import com.google.genai.types.Part;
import io.reactivex.rxjava3.core.Maybe;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
Expand Down Expand Up @@ -146,6 +150,39 @@ public void handleFunctionCalls_singleFunctionCall() {
.build());
}

@Test
public void handleFunctionCalls_longRunningToolWithEmptyResponse() {
InvocationContext invocationContext = createInvocationContext(createRootAgent());
Event event =
createEvent("event").toBuilder()
.content(
Content.fromParts(
Part.fromText("..."),
Part.builder()
.functionCall(
FunctionCall.builder()
.id("function_call_id")
.name("empty_tool")
.args(ImmutableMap.of())
.build())
.build()))
.build();
BaseTool tool =
new BaseTool("empty_tool", "Long-running tool without an immediate response", true) {
@Override
public Maybe<Map<String, Object>> runMaybeAsync(
Map<String, Object> args, ToolContext toolContext) {
return Maybe.empty();
}
};

Event functionResponseEvent =
Functions.handleFunctionCalls(invocationContext, event, ImmutableMap.of("empty_tool", tool))
.blockingGet();

assertThat(functionResponseEvent).isNull();
}

@Test
public void handleFunctionCalls_multipleFunctionCalls_parallel() {
InvocationContext invocationContext =
Expand Down
38 changes: 38 additions & 0 deletions core/src/test/java/com/google/adk/tools/BaseToolTest.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.google.adk.tools;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;

import com.google.adk.agents.InvocationContext;
import com.google.adk.agents.LlmAgent;
Expand All @@ -16,6 +17,7 @@
import com.google.genai.types.ToolCodeExecution;
import com.google.genai.types.UrlContext;
import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.rxjava3.core.Single;
import java.util.Map;
import java.util.Optional;
Expand Down Expand Up @@ -117,6 +119,42 @@ public Single<Map<String, Object>> runAsync(
.build());
}

@Test
public void runAsync_withOnlyRunMaybeAsyncOverride_returnsEmptyMap() {
BaseTool tool =
new BaseTool("test_tool", "test_description", /* isLongRunning= */ true) {
@Override
public Maybe<Map<String, Object>> runMaybeAsync(
Map<String, Object> args, ToolContext toolContext) {
return Maybe.empty();
}
};

assertThat(tool.runAsync(Map.of(), /* toolContext= */ null).blockingGet()).isEmpty();
}

@Test
public void runAsync_withRunAsyncAndRunMaybeAsyncOverridesCallingSuper_throwsException() {
BaseTool tool =
new BaseTool("test_tool", "test_description", /* isLongRunning= */ true) {
@Override
public Single<Map<String, Object>> runAsync(
Map<String, Object> args, ToolContext toolContext) {
return super.runAsync(args, toolContext);
}

@Override
public Maybe<Map<String, Object>> runMaybeAsync(
Map<String, Object> args, ToolContext toolContext) {
return super.runMaybeAsync(args, toolContext);
}
};

assertThrows(
UnsupportedOperationException.class,
() -> tool.runAsync(Map.of(), /* toolContext= */ null));
}

@Test
public void processLlmRequestWithGoogleSearchToolAddsToolToConfig() {
FunctionDeclaration functionDeclaration =
Expand Down
12 changes: 12 additions & 0 deletions core/src/test/java/com/google/adk/tools/FunctionToolTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,14 @@ public void call_withMaybeMapReturnType() throws Exception {
assertThat(result).containsExactly("key", "value");
}

@Test
public void runMaybeAsync_longRunningWithEmptyMaybeReturnType_returnsEmpty() throws Exception {
Method method = Functions.class.getMethod("returnsEmptyMaybeMap");
FunctionTool tool = new FunctionTool(null, method, /* isLongRunning= */ true);

assertThat(tool.runMaybeAsync(new HashMap<>(), null).blockingGet()).isNull();
}

@Test
public void create_withSingleMapReturnType() {
FunctionTool tool = FunctionTool.create(Functions.class, "returnsSingleMap");
Expand Down Expand Up @@ -804,6 +812,10 @@ public static Maybe<Map<String, Object>> returnsMaybeMap() {
return Maybe.just(ImmutableMap.of("key", "value"));
}

public static Maybe<Map<String, Object>> returnsEmptyMaybeMap() {
return Maybe.empty();
}

public static Maybe<String> returnsMaybeString() {
return Maybe.just("not supported");
}
Expand Down