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
28 changes: 19 additions & 9 deletions core/src/main/java/com/google/adk/tools/SetModelResponseTool.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.google.adk.tools;

import com.google.adk.SchemaUtils;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.genai.types.FunctionDeclaration;
import com.google.genai.types.Schema;
Expand Down Expand Up @@ -77,34 +78,43 @@ public Single<Map<String, Object>> runAsync(Map<String, Object> args, ToolContex
+ "\nRecall the set_model_response function correctly, fix the errors, and"
+ " call it again with all required fields using the correct types.");
}
// Match Python's model_dump(exclude_none=True) for Java's map-shaped response.
Map<String, Object> validatedResponse = excludeNullFields(args);
Map<String, Object> validatedResponse = excludeNullFields(args, outputSchema);
toolContext.actions().setSetModelResponse(validatedResponse);
return validatedResponse;
});
}

private static Map<String, Object> excludeNullFields(Map<String, Object> values) {
/**
* Drops nulls like Python's {@code model_dump(exclude_none=True)}, but keeps the null of a
* property that {@code schema} marks required and nullable, which the schema demands.
*/
private static Map<String, Object> excludeNullFields(Map<String, Object> values, Schema schema) {
// Validation passed, so each key has a property schema and each non-empty list an item schema.
Map<String, Schema> properties = schema.properties().orElseThrow();
List<String> required = schema.required().orElse(ImmutableList.of());
Map<String, Object> result = new LinkedHashMap<>();
for (Map.Entry<String, Object> entry : values.entrySet()) {
Schema property = properties.get(entry.getKey());
Object value = entry.getValue();
if (value != null) {
result.put(entry.getKey(), excludeNullsFromValue(value));
result.put(entry.getKey(), excludeNullsFromValue(value, property));
} else if (property.nullable().orElse(false) && required.contains(entry.getKey())) {
result.put(entry.getKey(), null);
}
}
return result;
}

private static Object excludeNullsFromValue(Object value) {
private static Object excludeNullsFromValue(Object value, Schema schema) {
if (value instanceof Map<?, ?>) {
@SuppressWarnings("unchecked") // Function-call args always have String keys.
Map<String, Object> map = (Map<String, Object>) value;
return excludeNullFields(map);
return excludeNullFields(map, schema);
}
if (value instanceof List<?>) {
if (value instanceof List<?> list) {
List<Object> result = new ArrayList<>();
for (Object item : (List<?>) value) {
result.add(excludeNullsFromValue(item));
for (Object item : list) {
result.add(excludeNullsFromValue(item, schema.items().orElseThrow()));
}
return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import static com.google.adk.testing.TestUtils.createTestLlm;
import static com.google.common.truth.Truth.assertThat;

import com.google.adk.SchemaUtils;
import com.google.adk.models.LlmResponse;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
Expand Down Expand Up @@ -123,6 +124,96 @@ public void runAsync_nullableNull_excludesNullFromReturnedAndRecordedResponse()
assertThat(toolContext.actions().setModelResponse()).hasValue(ImmutableMap.of("a", "x"));
}

@Test
public void runAsync_requiredNullableNull_keepsTheNull() {
Schema outputSchema =
Schema.builder()
.type("OBJECT")
.properties(
ImmutableMap.of(
"a",
Schema.builder().type("STRING").build(),
"b",
Schema.builder().type("STRING").nullable(true).build()))
.required(ImmutableList.of("a", "b"))
.build();
Map<String, Object> args = new LinkedHashMap<>();
args.put("a", "x");
args.put("b", null);

ToolContext toolContext = createToolContext();
Map<String, Object> result =
new SetModelResponseTool(outputSchema).runAsync(args, toolContext).blockingGet();

assertThat(result).containsExactly("a", "x", "b", null).inOrder();
assertThat(toolContext.actions().setModelResponse()).hasValue(result);
// Throws if the result does not match the schema, as dropping the null did.
SchemaUtils.validateMapOnSchema(result, outputSchema, /* isInput= */ false);
}

@Test
public void runAsync_nestedRequiredNullableNull_keepsTheNull() {
Schema outputSchema =
Schema.builder()
.type("OBJECT")
.properties(ImmutableMap.of("addr", requiredNullableZipAddressSchema()))
.required(ImmutableList.of("addr"))
.build();
Map<String, Object> address = new LinkedHashMap<>();
address.put("city", "NYC");
address.put("zip", null);

ToolContext toolContext = createToolContext();
Map<String, Object> result =
new SetModelResponseTool(outputSchema)
.runAsync(ImmutableMap.of("addr", address), toolContext)
.blockingGet();

assertThat(result).isEqualTo(ImmutableMap.of("addr", address));
assertThat(toolContext.actions().setModelResponse()).hasValue(result);
}

@Test
public void runAsync_arrayItemRequiredNullableNull_keepsTheNull() {
Schema outputSchema =
Schema.builder()
.type("OBJECT")
.properties(
ImmutableMap.of(
"addresses",
Schema.builder()
.type("ARRAY")
.items(requiredNullableZipAddressSchema())
.build()))
.required(ImmutableList.of("addresses"))
.build();
Map<String, Object> address = new LinkedHashMap<>();
address.put("city", "NYC");
address.put("zip", null);

ToolContext toolContext = createToolContext();
Map<String, Object> result =
new SetModelResponseTool(outputSchema)
.runAsync(ImmutableMap.of("addresses", ImmutableList.of(address)), toolContext)
.blockingGet();

assertThat(result).isEqualTo(ImmutableMap.of("addresses", ImmutableList.of(address)));
assertThat(toolContext.actions().setModelResponse()).hasValue(result);
}

private static Schema requiredNullableZipAddressSchema() {
return Schema.builder()
.type("OBJECT")
.properties(
ImmutableMap.of(
"city",
Schema.builder().type("STRING").build(),
"zip",
Schema.builder().type("STRING").nullable(true).build()))
.required(ImmutableList.of("city", "zip"))
.build();
}

@Test
public void runAsync_nestedNullableNull_excludesNullFromReturnedAndRecordedResponse() {
Schema addressSchema =
Expand Down
Loading