diff --git a/core/src/main/java/com/google/adk/codeexecutors/BaseCodeExecutor.java b/core/src/main/java/com/google/adk/codeexecutors/BaseCodeExecutor.java index b13d69a9..6555e050 100644 --- a/core/src/main/java/com/google/adk/codeexecutors/BaseCodeExecutor.java +++ b/core/src/main/java/com/google/adk/codeexecutors/BaseCodeExecutor.java @@ -61,19 +61,13 @@ public int errorRetryAttempts() { } /** - * The list of the enclosing delimiters to identify the code blocks. + * The list of enclosing delimiters to identify code blocks in model responses. * - *

Each inner list contains a pair of start and end delimiters. This supports multiple pairs of - * delimiters. + *

Each inner list should contain a pair of strings: the start delimiter and the end delimiter. + * This structure supports multiple pairs of delimiters. For example, `[["```tool_code\n", + * "\n```"], ["```python\n", "\n```"]]` defines two sets of delimiters. * - *

For example, the delimiter ('```python\n', '\n```') can be used to identify code blocks with - * the following format: - * - *

```python - * - *

print("hello") - * - *

``` + *

Defaults to `[["```tool_code\n", "\n```"], ["```python\n", "\n```"]]`. */ public ImmutableList> codeBlockDelimiters() { return CODE_BLOCK_DELIMITERS; diff --git a/core/src/main/java/com/google/adk/codeexecutors/CodeExecutionUtils.java b/core/src/main/java/com/google/adk/codeexecutors/CodeExecutionUtils.java index b9afdcaf..4d62f496 100644 --- a/core/src/main/java/com/google/adk/codeexecutors/CodeExecutionUtils.java +++ b/core/src/main/java/com/google/adk/codeexecutors/CodeExecutionUtils.java @@ -21,7 +21,9 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static java.util.stream.Collectors.joining; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import com.google.adk.JsonBaseModel; import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; @@ -211,7 +213,13 @@ public static Builder builder() { /** Builder for {@link CodeExecutionResult}. */ @AutoValue.Builder + @JsonPOJOBuilder(withPrefix = "") public abstract static class Builder { + @JsonCreator + static Builder create() { + return CodeExecutionResult.builder(); + } + public abstract Builder stdout(String stdout); public abstract Builder stderr(String stderr); @@ -243,7 +251,13 @@ public static Builder builder() { /** Builder for {@link CodeExecutionInput}. */ @AutoValue.Builder + @JsonPOJOBuilder(withPrefix = "") public abstract static class Builder { + @JsonCreator + static Builder create() { + return CodeExecutionInput.builder(); + } + public abstract Builder code(String code); public abstract Builder inputFiles(List inputFiles); @@ -273,7 +287,13 @@ public static Builder builder() { /** Builder for {@link File}. */ @AutoValue.Builder + @JsonPOJOBuilder(withPrefix = "") public abstract static class Builder { + @JsonCreator + static Builder create() { + return File.builder(); + } + public abstract Builder name(String name); public abstract Builder content(String content); diff --git a/core/src/main/java/com/google/adk/codeexecutors/CodeExecutorContext.java b/core/src/main/java/com/google/adk/codeexecutors/CodeExecutorContext.java index a3410222..e0de0f58 100644 --- a/core/src/main/java/com/google/adk/codeexecutors/CodeExecutorContext.java +++ b/core/src/main/java/com/google/adk/codeexecutors/CodeExecutorContext.java @@ -22,6 +22,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.adk.JsonBaseModel; import com.google.adk.codeexecutors.CodeExecutionUtils.File; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.time.InstantSource; import java.util.ArrayList; @@ -88,9 +89,10 @@ public void setExecutionId(String sessionId) { * * @return A list of processed file names in the code executor context. */ - public List getProcessedFileNames() { - return (List) - this.context.computeIfAbsent(PROCESSED_FILE_NAMES_KEY, unused -> new ArrayList<>()); + public ImmutableList getProcessedFileNames() { + return ImmutableList.copyOf( + (List) + this.context.computeIfAbsent(PROCESSED_FILE_NAMES_KEY, unused -> new ArrayList<>())); } /** @@ -110,7 +112,7 @@ public void addProcessedFileNames(List fileNames) { * * @return A list of input files in the code executor context. */ - public List getInputFiles() { + public ImmutableList getInputFiles() { List> fileMaps = (List>) this.sessionState.getOrDefault(INPUT_FILE_KEY, new ArrayList<>()); diff --git a/core/src/main/java/com/google/adk/codeexecutors/ContainerCodeExecutor.java b/core/src/main/java/com/google/adk/codeexecutors/ContainerCodeExecutor.java index 1d1202ea..ea0de9bf 100644 --- a/core/src/main/java/com/google/adk/codeexecutors/ContainerCodeExecutor.java +++ b/core/src/main/java/com/google/adk/codeexecutors/ContainerCodeExecutor.java @@ -36,7 +36,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** A code executor that uses a custom container to execute code. */ +/** + * A code executor that uses a custom container to execute code. + * + *

This implementation uses blocking I/O to interact with Docker via {@code docker-java} library. + */ public class ContainerCodeExecutor extends BaseCodeExecutor { private static final Logger logger = LoggerFactory.getLogger(ContainerCodeExecutor.class); private static final String DEFAULT_IMAGE_TAG = "adk-code-executor:latest"; @@ -91,6 +95,11 @@ public boolean optimizeDataFile() { return false; } + /** + * {@inheritDoc} + * + *

This method blocks waiting for container execution to complete. + */ @Override public CodeExecutionResult executeCode( InvocationContext invocationContext, CodeExecutionInput codeExecutionInput) { diff --git a/core/src/test/java/com/google/adk/codeexecutors/BaseCodeExecutorTest.java b/core/src/test/java/com/google/adk/codeexecutors/BaseCodeExecutorTest.java new file mode 100644 index 00000000..be66ec44 --- /dev/null +++ b/core/src/test/java/com/google/adk/codeexecutors/BaseCodeExecutorTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.adk.codeexecutors; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.adk.agents.InvocationContext; +import com.google.adk.codeexecutors.CodeExecutionUtils.CodeExecutionInput; +import com.google.adk.codeexecutors.CodeExecutionUtils.CodeExecutionResult; +import com.google.common.collect.ImmutableList; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class BaseCodeExecutorTest { + + private static class TestCodeExecutor extends BaseCodeExecutor { + @Override + public CodeExecutionResult executeCode( + InvocationContext invocationContext, CodeExecutionInput codeExecutionInput) { + return null; + } + } + + @Test + public void baseCodeExecutor_defaultValues() { + TestCodeExecutor codeExecutor = new TestCodeExecutor(); + assertThat(codeExecutor.optimizeDataFile()).isFalse(); + assertThat(codeExecutor.stateful()).isFalse(); + assertThat(codeExecutor.errorRetryAttempts()).isEqualTo(2); + assertThat(codeExecutor.codeBlockDelimiters()) + .isEqualTo( + ImmutableList.of( + ImmutableList.of("```tool_code\n", "\n```"), + ImmutableList.of("```python\n", "\n```"))); + assertThat(codeExecutor.executionResultDelimiters()) + .isEqualTo(ImmutableList.of("```tool_output\n", "\n```")); + } +} diff --git a/core/src/test/java/com/google/adk/codeexecutors/BuiltInCodeExecutorTest.java b/core/src/test/java/com/google/adk/codeexecutors/BuiltInCodeExecutorTest.java new file mode 100644 index 00000000..54306fa2 --- /dev/null +++ b/core/src/test/java/com/google/adk/codeexecutors/BuiltInCodeExecutorTest.java @@ -0,0 +1,67 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.adk.codeexecutors; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.adk.agents.InvocationContext; +import com.google.adk.codeexecutors.CodeExecutionUtils.CodeExecutionInput; +import com.google.adk.models.LlmRequest; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class BuiltInCodeExecutorTest { + + private BuiltInCodeExecutor codeExecutor; + + @Before + public void setUp() { + codeExecutor = new BuiltInCodeExecutor(); + } + + @Test + public void executeCode_throwsUnsupportedOperationException() { + assertThrows( + UnsupportedOperationException.class, + () -> + codeExecutor.executeCode( + InvocationContext.builder().build(), + CodeExecutionInput.builder().code("code").build())); + } + + @Test + public void processLlmRequest_gemini2Model_addsCodeExecutionTool() { + LlmRequest.Builder llmRequestBuilder = LlmRequest.builder().model("gemini-2.0-flash-exp"); + codeExecutor.processLlmRequest(llmRequestBuilder); + LlmRequest llmRequest = llmRequestBuilder.build(); + assertThat(llmRequest.config()).isPresent(); + assertThat(llmRequest.config().get().tools()).isPresent(); + assertThat(llmRequest.config().get().tools().get()).hasSize(1); + assertThat(llmRequest.config().get().tools().get().get(0).codeExecution()).isPresent(); + } + + @Test + public void processLlmRequest_nonGemini2Model_throwsIllegalArgumentException() { + LlmRequest.Builder llmRequestBuilder = LlmRequest.builder().model("gemini-1.5-pro"); + assertThrows( + IllegalArgumentException.class, () -> codeExecutor.processLlmRequest(llmRequestBuilder)); + } +} diff --git a/core/src/test/java/com/google/adk/codeexecutors/CodeExecutionUtilsTest.java b/core/src/test/java/com/google/adk/codeexecutors/CodeExecutionUtilsTest.java new file mode 100644 index 00000000..9cbb4ed8 --- /dev/null +++ b/core/src/test/java/com/google/adk/codeexecutors/CodeExecutionUtilsTest.java @@ -0,0 +1,116 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.adk.codeexecutors; + +import static com.google.common.truth.Truth8.assertThat; + +import com.google.adk.codeexecutors.CodeExecutionUtils.CodeExecutionResult; +import com.google.common.collect.ImmutableList; +import com.google.genai.types.CodeExecutionResult.Outcome; +import com.google.genai.types.Content; +import com.google.genai.types.Part; +import java.util.Optional; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class CodeExecutionUtilsTest { + + @Test + public void buildCodeExecutionResultPart_success() { + CodeExecutionResult result = CodeExecutionResult.builder().stdout("output").build(); + Part part = CodeExecutionUtils.buildCodeExecutionResultPart(result); + assertThat(part.codeExecutionResult()).isPresent(); + assertThat(part.codeExecutionResult().get().outcome()).hasValue(Outcome.OK); + assertThat(part.codeExecutionResult().get().output()) + .hasValue("Code execution result:\noutput\n"); + } + + @Test + public void buildCodeExecutionResultPart_failure() { + CodeExecutionResult result = CodeExecutionResult.builder().stderr("error").build(); + Part part = CodeExecutionUtils.buildCodeExecutionResultPart(result); + assertThat(part.codeExecutionResult()).isPresent(); + assertThat(part.codeExecutionResult().get().outcome()).hasValue(Outcome.FAILED); + assertThat(part.codeExecutionResult().get().output()).hasValue("error"); + } + + @Test + public void buildExecutableCodePart_success() { + Part part = CodeExecutionUtils.buildExecutableCodePart("code"); + assertThat(part.executableCode()).isPresent(); + assertThat(part.executableCode().get().code()).hasValue("code"); + } + + @Test + public void convertCodeExecutionParts_executableCode() { + Content content = + Content.builder() + .parts(ImmutableList.of(CodeExecutionUtils.buildExecutableCodePart("code"))) + .role("model") + .build(); + Content newContent = + CodeExecutionUtils.convertCodeExecutionParts( + content, ImmutableList.of("```", "```"), ImmutableList.of()); + assertThat(newContent.parts().get()).hasSize(1); + assertThat(newContent.parts().get().get(0).text()).hasValue("```code```"); + } + + @Test + public void convertCodeExecutionParts_codeExecutionResult() { + Content content = + Content.builder() + .parts( + ImmutableList.of( + CodeExecutionUtils.buildCodeExecutionResultPart( + CodeExecutionResult.builder().stdout("output").build()))) + .role("model") + .build(); + Content newContent = + CodeExecutionUtils.convertCodeExecutionParts( + content, ImmutableList.of(), ImmutableList.of("'''", "'''")); + assertThat(newContent.parts().get()).hasSize(1); + assertThat(newContent.parts().get().get(0).text().get()).contains("'''"); + assertThat(newContent.parts().get().get(0).text().get()).contains("output"); + } + + @Test + public void extractCodeAndTruncateContent_executableCode() { + Content.Builder contentBuilder = + Content.builder() + .parts(ImmutableList.of(CodeExecutionUtils.buildExecutableCodePart("code"))) + .role("model"); + Optional code = + CodeExecutionUtils.extractCodeAndTruncateContent( + contentBuilder, ImmutableList.of(ImmutableList.of("```", "```"))); + assertThat(code).hasValue("code"); + assertThat(contentBuilder.build().parts().get()).hasSize(1); + } + + @Test + public void extractCodeAndTruncateContent_text() { + Content.Builder contentBuilder = + Content.builder().parts(ImmutableList.of(Part.fromText("```code```"))).role("model"); + Optional code = + CodeExecutionUtils.extractCodeAndTruncateContent( + contentBuilder, ImmutableList.of(ImmutableList.of("```", "```"))); + assertThat(code).hasValue("code"); + assertThat(contentBuilder.build().parts().get()).hasSize(1); + assertThat(contentBuilder.build().parts().get().get(0).executableCode()).isPresent(); + } +} diff --git a/core/src/test/java/com/google/adk/codeexecutors/CodeExecutorContextTest.java b/core/src/test/java/com/google/adk/codeexecutors/CodeExecutorContextTest.java new file mode 100644 index 00000000..c4a9f418 --- /dev/null +++ b/core/src/test/java/com/google/adk/codeexecutors/CodeExecutorContextTest.java @@ -0,0 +1,55 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.adk.codeexecutors; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.adk.codeexecutors.CodeExecutionUtils.File; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class CodeExecutorContextTest { + + @Test + public void getProcessedFileNames_returnsImmutableList() { + CodeExecutorContext context = new CodeExecutorContext(new HashMap<>()); + context.addProcessedFileNames(Arrays.asList("file1.txt")); + ImmutableList processedFileNames = context.getProcessedFileNames(); + assertThat(processedFileNames).containsExactly("file1.txt"); + } + + @Test + public void getInputFiles_returnsImmutableList() { + Map sessionState = new HashMap<>(); + sessionState.put( + "_code_executor_input_files", + ImmutableList.of( + ImmutableMap.of("name", "file2.txt", "content", "content", "mimeType", "text/plain"))); + CodeExecutorContext context = new CodeExecutorContext(sessionState); + ImmutableList inputFiles = context.getInputFiles(); + assertThat(inputFiles) + .containsExactly( + File.builder().name("file2.txt").content("content").mimeType("text/plain").build()); + } +} diff --git a/core/src/test/java/com/google/adk/codeexecutors/ContainerCodeExecutorTest.java b/core/src/test/java/com/google/adk/codeexecutors/ContainerCodeExecutorTest.java new file mode 100644 index 00000000..809a227b --- /dev/null +++ b/core/src/test/java/com/google/adk/codeexecutors/ContainerCodeExecutorTest.java @@ -0,0 +1,35 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.adk.codeexecutors; + +import static org.junit.Assert.assertThrows; + +import java.util.Optional; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class ContainerCodeExecutorTest { + + @Test + public void constructor_emptyImageAndDockerPath_throwsIllegalArgumentException() { + assertThrows( + IllegalArgumentException.class, + () -> new ContainerCodeExecutor(Optional.empty(), Optional.empty(), Optional.empty())); + } +}