diff --git a/core/pom.xml b/core/pom.xml
index fe65715f3..7d2032c7e 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -1,4 +1,4 @@
-
+
4.0.0
-
com.google.adk
google-adk-parent
- 0.4.1-SNAPSHOT
+ 0.4.1-SNAPSHOT
+
-
google-adk
Agent Development Kit
Agent Development Kit: an open-source, code-first toolkit designed to simplify building, evaluating, and deploying advanced AI agents anywhere.
-
-
-
com.anthropic
@@ -201,6 +197,15 @@
maven-compiler-plugin
+
+ io.spring.javaformat
+ spring-javaformat-maven-plugin
+ 0.0.40
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/core/src/test/java/com/google/adk/models/ClaudeGenerateContentTest.java.invalid b/core/src/test/java/com/google/adk/models/ClaudeGenerateContentTest.java.invalid
new file mode 100644
index 000000000..9c520f094
--- /dev/null
+++ b/core/src/test/java/com/google/adk/models/ClaudeGenerateContentTest.java.invalid
@@ -0,0 +1,601 @@
+//This test file is marked invalid as it contains compilation errors. Change the extension to of this file to .java, to manually edit its contents
+/*
+ * Copyright 2025 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.
+ */
+// ********RoostGPT********
+/*
+Test generated by RoostGPT for test unit-java-adk using AI Type Azure Open AI and AI Model gpt-4.1
+
+ROOST_METHOD_HASH=generateContent_bc4a182290
+ROOST_METHOD_SIG_HASH=generateContent_c08b9769fe
+
+Scenario 1: Basic request with minimal parameters
+
+Details:
+ TestName: generateContentWithMinimalLlmRequest
+ Description: This test verifies that the generateContent method can handle a basic LlmRequest with only content and no configuration, tools, or system instructions. It should correctly map and process the input content and return a Flowable without errors.
+Execution:
+ Arrange: Mock an LlmRequest containing one Content with simple text. Mock the anthropicClient to return a valid Message for this input. No tools or special configuration are included.
+ Act: Call generateContent with the created LlmRequest and stream set to false.
+ Assert: Validate that the returned Flowable contains the expected LlmResponse built from the mocked message response.
+Validation:
+ The assertion ensures that the method processes the minimal input correctly and does not depend on optional parameters. This proves the method's robustness for simple use cases.
+
+Scenario 2: LlmRequest with system instruction
+
+Details:
+ TestName: generateContentWithSystemInstruction
+ Description: This test checks that generateContent copies system instructions from GenerateContentConfig into the MessageCreateParams and passes them to the underlying client appropriately.
+Execution:
+ Arrange: Setup an LlmRequest with a GenerateContentConfig containing a Content whose parts include text. Mock the anthropicClient to return a message.
+ Act: Call generateContent with this request and stream set to false.
+ Assert: Validate that the system text in MessageCreateParams matches the expected concatenated system instruction text captured from the config.
+Validation:
+ This confirms correct extraction and assignment of system instructions, which is critical for proper context setting in generative models.
+
+Scenario 3: LlmRequest with multiple Content parts
+
+Details:
+ TestName: generateContentWithMultipleContentParts
+ Description: Intended to check if the method supports multiple parts in a Content and correctly transforms each into MessageParam block parameters.
+Execution:
+ Arrange: Create an LlmRequest with Content containing several parts (e.g., two text parts and one functionCall part). Mock the anthropicClient to return a message reflecting this structure.
+ Act: Call generateContent with the prepared request.
+ Assert: Ensure that the resulting LlmResponse's content matches the structure provided in the request.
+Validation:
+ Verifies that message conversion logic and part mapping work as expected for multiple content blocks, affirming support for complex input.
+
+Scenario 4: LlmRequest with tools in config
+
+Details:
+ TestName: generateContentWithToolsConfiguration
+ Description: Validates that when tools are present in GenerateContentConfig, they are correctly converted and included in MessageCreateParams.
+Execution:
+ Arrange: Set up an LlmRequest with config containing tools, including one functionDeclaration. Mock conversion methods and anthropicClient to produce an appropriate response.
+ Act: Invoke generateContent with this request.
+ Assert: Check that tools are present in MessageCreateParams and toolChoice is handled as per logic.
+Validation:
+ This confirms integration of tool configuration, needed for advanced use cases requiring function call support.
+
+Scenario 5: LlmRequest with toolChoice and tools field non-empty
+
+Details:
+ TestName: generateContentWithToolChoiceAuto
+ Description: Tests that toolChoice is set to auto when the tools field in LlmRequest is non-empty, and that parallel tool use is disabled.
+Execution:
+ Arrange: Mock an LlmRequest with tools (not empty). Ensure anthropicClient returns a message. Mock toolChoice creation as per method logic.
+ Act: Call generateContent.
+ Assert: Validate that toolChoice is 'auto', with parallel tool use explicitly disabled. Verify tools are passed appropriately.
+Validation:
+ The test checks business logic around toolChoice, which is important for controlling concurrency and tool behavior in Anthropic messages.
+
+Scenario 6: LlmRequest with missing model parameter
+
+Details:
+ TestName: generateContentWithDefaultModelFallback
+ Description: Verifies that if model() is not present in LlmRequest, the method defaults to the Claude instance’s model name.
+Execution:
+ Arrange: Mock an LlmRequest where model() returns Optional.empty(). Set up anthropicClient to process accordingly.
+ Act: Call generateContent.
+ Assert: Check that MessageCreateParams uses the Claude instance's model name, not an empty or undefined value.
+Validation:
+ This confirms fallback behavior for missing models, ensuring the method operates reliably when the client omits model details.
+
+Scenario 7: LlmRequest with no config, system instruction, or tools
+
+Details:
+ TestName: generateContentWithNoConfigOrInstructions
+ Description: Ensures the method behaves correctly when configuration, system instruction, and tools are all absent from the request.
+Execution:
+ Arrange: Create an LlmRequest with none of the optional parameters set. Mock anthropicClient as usual.
+ Act: Invoke generateContent.
+ Assert: Assert that MessageCreateParams uses an empty string for system and no tools/toolChoice.
+Validation:
+ Demonstrates that the method complies with defaults and does not raise errors due to missing optional data.
+
+Scenario 8: LlmRequest with config but empty tools and no function declarations
+
+Details:
+ TestName: generateContentConfigWithEmptyToolsNoFunctionDeclarations
+ Description: Checks the method's handling of the tools list when config is present but tools are empty or lack function declarations.
+Execution:
+ Arrange: Mock an LlmRequest with config containing empty tools and no function declarations. anthropicClient returns a message.
+ Act: Call generateContent.
+ Assert: Ensure that tools in MessageCreateParams are an empty ImmutableList, and toolChoice is null.
+Validation:
+ The test assures that the method gracefully skips tool integration if function declarations aren't available, avoiding unnecessary conversions.
+
+Scenario 9: Content part with functionCall present
+
+Details:
+ TestName: generateContentWithFunctionCallPart
+ Description: Validates conversion logic for Content parts which have a functionCall, transforming them into tool use blocks for MessageParam.
+Execution:
+ Arrange: Create a Content in LlmRequest with one part being a functionCall. Mock anthropicClient to recognize and return a corresponding message block.
+ Act: Call generateContent.
+ Assert: Confirm that the part is converted into a ToolUseBlockParam and integrated into message blocks.
+Validation:
+ Proves detailed mapping of functionCall-based parts, supporting tool and function execution.
+
+Scenario 10: Content part with functionResponse present
+
+Details:
+ TestName: generateContentWithFunctionResponsePart
+ Description: Checks logic for handling Content parts that have functionResponse, ensuring it's mapped to ToolResultBlockParam.
+Execution:
+ Arrange: Construct an LlmRequest with Content containing a part with functionResponse and a "result" response key. Mock the anthropicClient's response.
+ Act: Call generateContent.
+ Assert: Validate that conversion places ToolResultBlockParam with appropriate id and result content into the message.
+Validation:
+ Ensures function responses are correctly propagated for downstream clients or workflows.
+
+Scenario 11: Content parts with both text and function structures
+
+Details:
+ TestName: generateContentWithMixedPartTypes
+ Description: Tests the method’s ability to process mixed Content parts: texts, functionCalls, and functionResponses all in the same request.
+Execution:
+ Arrange: Mock LlmRequest with Content having text, functionCall, and functionResponse parts. Mock anthropicClient for a composite response.
+ Act: Invoke generateContent.
+ Assert: Assert that all parts are represented correctly in LlmResponse and MessageParams.
+Validation:
+ Verifies robust parsing and conversion in complex composite requests.
+
+Scenario 12: LlmRequest with maxTokens override in Claude instance
+
+Details:
+ TestName: generateContentRespectsMaxTokens
+ Description: Ensures that the MessageCreateParams.maxTokens field is set to Claude.maxTokens and respects the override at instance level.
+Execution:
+ Arrange: Create a Claude instance with custom maxTokens value. Use a typical LlmRequest and mock anthropicClient.
+ Act: Call generateContent.
+ Assert: Confirm that MessageCreateParams.maxTokens matches the Claude instance's configured value.
+Validation:
+ This secures token limits as intended, critical for model output management.
+
+Scenario 13: LlmRequest with empty contents list
+
+Details:
+ TestName: generateContentWithEmptyContentsList
+ Description: Evaluates edge-case behavior when LlmRequest.contents returns an empty list. The method should not throw and should return a valid Flowable.
+Execution:
+ Arrange: Set up an LlmRequest with contents as an empty ImmutableList. Mock anthropicClient with a response for this case.
+ Act: Call generateContent.
+ Assert: Ensure Flowable is returned and that default Content is handled.
+Validation:
+ This ensures method resilience against empty input, avoiding null pointer exceptions or similar failures.
+
+Scenario 14: Passing stream parameter as true
+
+Details:
+ TestName: generateContentWithStreamTrueParameter
+ Description: Verifies that passing stream=true does not alter the output, since streaming is presently not supported and the method ignores the parameter.
+Execution:
+ Arrange: Use a standard LlmRequest and mock anthropicClient. Pass stream as true.
+ Act: Call generateContent.
+ Assert: Assert that behavior is identical to stream=false and method returns a single Flowable.
+Validation:
+ Guarantees forward compatibility and prevents failures in client implementations that may set stream=true.
+
+Scenario 15: Client returns a null Message
+
+Details:
+ TestName: generateContentHandlesNullMessage
+ Description: Examines how generateContent handles a case where anthropicClient.messages().create returns null, ensuring it does not throw and returns a safe LlmResponse.
+Execution:
+ Arrange: Mock anthropicClient to return null from messages().create. Use a standard LlmRequest.
+ Act: Call generateContent.
+ Assert: Validate that the method returns a Flowable representing an empty or default response, and handles null robustly.
+Validation:
+ Important for defensive coding, avoiding null pointer exceptions and ensuring service reliability.
+
+Scenario 16: Unsupported Content part type
+
+Details:
+ TestName: generateContentThrowsOnUnsupportedPart
+ Description: Tests that the method throws UnsupportedOperationException if a Content part has an unsupported structure (e.g., none of text, functionCall, functionResponse is present).
+Execution:
+ Arrange: Mock a Content part without the expected fields. Use it in LlmRequest and attempt to call generateContent.
+ Act: Call generateContent.
+ Assert: Confirm that an UnsupportedOperationException is thrown at the expected conversion point.
+Validation:
+ Confirms that the method’s fail-fast logic is correct and that unsupported features are explicitly flagged.
+
+*/
+
+// ********RoostGPT********
+
+package com.google.adk.models;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+import com.anthropic.client.AnthropicClient;
+import com.anthropic.models.messages.ContentBlock;
+import com.anthropic.models.messages.Message;
+import com.anthropic.models.messages.MessageCreateParams;
+import com.anthropic.models.messages.ToolChoice;
+import com.anthropic.models.messages.ToolChoiceAuto;
+import com.anthropic.models.messages.ToolUnion;
+import com.fasterxml.jackson.databind.ObjectMapper;
+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.FunctionDeclaration;
+import com.google.genai.types.GenerateContentConfig;
+import com.google.genai.types.Part;
+import io.reactivex.rxjava3.core.Flowable;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import java.util.*;
+import com.anthropic.models.messages.ContentBlockParam;
+import com.anthropic.models.messages.MessageParam;
+import com.anthropic.models.messages.MessageParam.Role;
+import com.anthropic.models.messages.TextBlockParam;
+import com.anthropic.models.messages.Tool;
+import com.anthropic.models.messages.ToolResultBlockParam;
+import com.anthropic.models.messages.ToolUseBlockParam;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ExtendWith(MockitoExtension.class)
+public abstract class ClaudeGenerateContentTest {
+ @Mock
+ private AnthropicClient anthropicClient;
+ @Mock
+ private AnthropicClient.Messages mockMessages;
+ private Claude claude;
+ private static final String DEFAULT_MODEL = "claude-instant-v1";
+ @BeforeEach
+ public void setup() {
+ when(anthropicClient.messages()).thenReturn(mockMessages);
+ claude = new Claude(DEFAULT_MODEL, anthropicClient);
+ }
+ @Test
+ @Tag("valid")
+ public void testGenerateContentWithMinimalLlmRequest() {
+ Part part = Part.builder().text("Hello world!").build();
+ Content content = Content.builder().role("user").parts(ImmutableList.of(part)).build();
+ LlmRequest llmRequest = mock(LlmRequest.class);
+ when(llmRequest.contents()).thenReturn(ImmutableList.of(content));
+ when(llmRequest.config()).thenReturn(Optional.empty());
+ when(llmRequest.model()).thenReturn(Optional.of(DEFAULT_MODEL));
+ when(llmRequest.tools()).thenReturn(Collections.emptyMap());
+ Message message = Message.builder().content(ImmutableList.of(ContentBlock.ofText("response content"))).build();
+ when(mockMessages.create(any(MessageCreateParams.class))).thenReturn(message);
+ Flowable resultFlowable = claude.generateContent(llmRequest, false);
+ LlmResponse result = resultFlowable.blockingFirst();
+ assertNotNull(result, "LlmResponse must not be null");
+ assertNotNull(result.content(), "LlmResponse content must not be null");
+ assertEquals("model", result.content().role(), "Content role should be 'model'");
+ assertFalse(((List>)result.content().parts().orElse(ImmutableList.of())).isEmpty(), "Parts should not be empty");
+ assertEquals("response content", ((List)result.content().parts().get()).get(0).text().get(), "Should match response part");
+ }
+ @Test
+ @Tag("valid")
+ public void testGenerateContentWithSystemInstruction() {
+ Part sysPart = Part.builder().text("System: Do awesome stuff").build();
+ Content sysContent = Content.builder().role("system").parts(ImmutableList.of(sysPart)).build();
+ GenerateContentConfig config = mock(GenerateContentConfig.class);
+ when(config.systemInstruction()).thenReturn(Optional.of(sysContent));
+ LlmRequest llmRequest = mock(LlmRequest.class);
+ Content userContent = Content.builder().role("user").parts(ImmutableList.of(Part.builder().text("Say hi").build())).build();
+ when(llmRequest.contents()).thenReturn(ImmutableList.of(userContent));
+ when(llmRequest.config()).thenReturn(Optional.of(config));
+ when(llmRequest.model()).thenReturn(Optional.of(DEFAULT_MODEL));
+ when(llmRequest.tools()).thenReturn(Collections.emptyMap());
+ when(config.tools()).thenReturn(Optional.empty());
+ Message message = Message.builder().content(ImmutableList.of(ContentBlock.ofText("ok"))).build();
+ ArgumentCaptor paramsCaptor = ArgumentCaptor.forClass(MessageCreateParams.class);
+ when(mockMessages.create(paramsCaptor.capture())).thenReturn(message);
+ claude.generateContent(llmRequest, false).blockingFirst();
+ MessageCreateParams params = (MessageCreateParams) paramsCaptor.getValue();
+ assertEquals("System: Do awesome stuff", params.getSystem(), "System instruction should be set from config");
+ }
+ @Test
+ @Tag("valid")
+ public void testGenerateContentWithMultipleContentParts() {
+ Part textPart1 = Part.builder().text("One").build();
+ Part textPart2 = Part.builder().text("Two").build();
+ FunctionCall functionCall = FunctionCall.builder().id("fc1").name("foo").args(ImmutableMap.of("k", "v")).build();
+ Part callPart = Part.builder().functionCall(functionCall).build();
+ Content content = Content.builder().role("user").parts(ImmutableList.of(textPart1, textPart2, callPart)).build();
+ LlmRequest llmRequest = mock(LlmRequest.class);
+ when(llmRequest.contents()).thenReturn(ImmutableList.of(content));
+ when(llmRequest.config()).thenReturn(Optional.empty());
+ when(llmRequest.model()).thenReturn(Optional.of(DEFAULT_MODEL));
+ when(llmRequest.tools()).thenReturn(Collections.emptyMap());
+ ContentBlock cb1 = ContentBlock.ofText("One");
+ ContentBlock cb2 = ContentBlock.ofText("Two");
+ ContentBlock cb3 = ContentBlock.ofToolUse("fc1", "foo", com.anthropic.core.JsonValue.from(ImmutableMap.of("k", "v")));
+ Message message = Message.builder().content(ImmutableList.of(cb1, cb2, cb3)).build();
+ when(mockMessages.create(any())).thenReturn(message);
+ LlmResponse actual = claude.generateContent(llmRequest, false).blockingFirst();
+ assertNotNull(actual.content());
+ List parts = (List) actual.content().parts().orElse(ImmutableList.of());
+ assertEquals(3, parts.size(), "Three parts expected");
+ assertEquals("One", parts.get(0).text().orElse(""), "First part text");
+ assertEquals("Two", parts.get(1).text().orElse(""), "Second part text");
+ assertTrue(parts.get(2).functionCall().isPresent(), "Third part should be function call");
+ assertEquals("foo", parts.get(2).functionCall().get().name().orElse(""), "Function call name");
+ }
+ @Test
+ @Tag("integration")
+ public void testGenerateContentWithToolsConfiguration() {
+ FunctionDeclaration fnDecl = FunctionDeclaration.builder()
+ .name("doIt")
+ .description("desc")
+ .build();
+ GenerateContentConfig.ToolsConfig toolsConfig = mock(GenerateContentConfig.ToolsConfig.class);
+ when(toolsConfig.functionDeclarations()).thenReturn(Optional.of(ImmutableList.of(fnDecl)));
+ GenerateContentConfig config = mock(GenerateContentConfig.class);
+ when(config.tools()).thenReturn(Optional.of(ImmutableList.of(toolsConfig)));
+ when(config.systemInstruction()).thenReturn(Optional.empty());
+ LlmRequest llmRequest = mock(LlmRequest.class);
+ Content content = Content.builder().role("user").parts(ImmutableList.of(Part.builder().text("use tool").build())).build();
+ when(llmRequest.contents()).thenReturn(ImmutableList.of(content));
+ when(llmRequest.config()).thenReturn(Optional.of(config));
+ when(llmRequest.model()).thenReturn(Optional.of(DEFAULT_MODEL));
+ when(llmRequest.tools()).thenReturn(Collections.emptyMap());
+ Message message = Message.builder().content(ImmutableList.of(ContentBlock.ofText("used tool"))).build();
+ ArgumentCaptor paramCaptor = ArgumentCaptor.forClass(MessageCreateParams.class);
+ when(mockMessages.create(paramCaptor.capture())).thenReturn(message);
+ claude.generateContent(llmRequest, false).blockingFirst();
+ MessageCreateParams params = (MessageCreateParams) paramCaptor.getValue();
+ assertNotNull(params.getTools());
+ assertFalse(((List>)params.getTools()).isEmpty(), "Tools should not be empty");
+ assertNull(params.getToolChoice(), "ToolChoice should be null when tools field in LlmRequest is empty");
+ }
+ @Test
+ @Tag("integration")
+ public void testGenerateContentWithToolChoiceAuto() {
+ Content userContent = Content.builder().role("user").parts(ImmutableList.of(Part.builder().text("tool needed").build())).build();
+ LlmRequest llmRequest = mock(LlmRequest.class);
+ when(llmRequest.contents()).thenReturn(ImmutableList.of(userContent));
+ when(llmRequest.config()).thenReturn(Optional.empty());
+ when(llmRequest.model()).thenReturn(Optional.of(DEFAULT_MODEL));
+ Map dummyToolMap = new HashMap<>();
+ dummyToolMap.put("key", mock(BaseTool.class));
+ when(llmRequest.tools()).thenReturn(dummyToolMap);
+ Message message = Message.builder().content(ImmutableList.of(ContentBlock.ofText("result"))).build();
+ ArgumentCaptor paramCaptor = ArgumentCaptor.forClass(MessageCreateParams.class);
+ when(mockMessages.create(paramCaptor.capture())).thenReturn(message);
+ claude.generateContent(llmRequest, false).blockingFirst();
+ MessageCreateParams params = (MessageCreateParams) paramCaptor.getValue();
+ assertNotNull(params.getToolChoice());
+ assertTrue(params.getToolChoice() instanceof ToolChoice, "ToolChoice should be auto type");
+ ToolChoice toolChoice = (ToolChoice) params.getToolChoice();
+ ToolChoiceAuto auto = (ToolChoiceAuto) toolChoice.getAuto().get();
+ assertTrue(auto.disableParallelToolUse(), "Parallel tool use should be disabled");
+ }
+ @Test
+ @Tag("valid")
+ public void testGenerateContentWithDefaultModelFallback() {
+ Content userContent = Content.builder().role("user").parts(ImmutableList.of(Part.builder().text("no model").build())).build();
+ LlmRequest llmRequest = mock(LlmRequest.class);
+ when(llmRequest.contents()).thenReturn(ImmutableList.of(userContent));
+ when(llmRequest.config()).thenReturn(Optional.empty());
+ when(llmRequest.model()).thenReturn(Optional.empty());
+ when(llmRequest.tools()).thenReturn(Collections.emptyMap());
+ Message message = Message.builder().content(ImmutableList.of(ContentBlock.ofText("default model"))).build();
+ ArgumentCaptor paramCaptor = ArgumentCaptor.forClass(MessageCreateParams.class);
+ when(mockMessages.create(paramCaptor.capture())).thenReturn(message);
+ claude.generateContent(llmRequest, false).blockingFirst();
+ assertEquals(DEFAULT_MODEL, paramCaptor.getValue().getModel(), "Model fallback to Claude instance's model");
+ }
+ @Test
+ @Tag("valid")
+ public void testGenerateContentWithNoConfigOrInstructions() {
+ Content content = Content.builder().role("user").parts(ImmutableList.of(Part.builder().text("only content").build())).build();
+ LlmRequest llmRequest = mock(LlmRequest.class);
+ when(llmRequest.contents()).thenReturn(ImmutableList.of(content));
+ when(llmRequest.config()).thenReturn(Optional.empty());
+ when(llmRequest.model()).thenReturn(Optional.of(DEFAULT_MODEL));
+ when(llmRequest.tools()).thenReturn(Collections.emptyMap());
+ Message message = Message.builder().content(ImmutableList.of(ContentBlock.ofText("plain"))).build();
+ ArgumentCaptor paramCaptor = ArgumentCaptor.forClass(MessageCreateParams.class);
+ when(mockMessages.create(paramCaptor.capture())).thenReturn(message);
+ claude.generateContent(llmRequest, false).blockingFirst();
+ MessageCreateParams params = (MessageCreateParams) paramCaptor.getValue();
+ assertEquals("", params.getSystem(), "Empty string for system in params");
+ assertTrue(((List>)params.getTools()).isEmpty(), "No tools expected");
+ assertNull(params.getToolChoice(), "ToolChoice should be null");
+ }
+ @Test
+ @Tag("boundary")
+ public void testGenerateContentConfigWithEmptyToolsNoFunctionDeclarations() {
+ GenerateContentConfig.ToolsConfig toolsConfig = mock(GenerateContentConfig.ToolsConfig.class);
+ when(toolsConfig.functionDeclarations()).thenReturn(Optional.empty());
+ GenerateContentConfig config = mock(GenerateContentConfig.class);
+ when(config.tools()).thenReturn(Optional.of(ImmutableList.of(toolsConfig)));
+ when(config.systemInstruction()).thenReturn(Optional.empty());
+ LlmRequest llmRequest = mock(LlmRequest.class);
+ Content content = Content.builder().role("user").parts(ImmutableList.of(Part.builder().text("prompt").build())).build();
+ when(llmRequest.contents()).thenReturn(ImmutableList.of(content));
+ when(llmRequest.config()).thenReturn(Optional.of(config));
+ when(llmRequest.model()).thenReturn(Optional.of(DEFAULT_MODEL));
+ when(llmRequest.tools()).thenReturn(Collections.emptyMap());
+ Message message = Message.builder().content(ImmutableList.of(ContentBlock.ofText("ok"))).build();
+ ArgumentCaptor paramCaptor = ArgumentCaptor.forClass(MessageCreateParams.class);
+ when(mockMessages.create(paramCaptor.capture())).thenReturn(message);
+ claude.generateContent(llmRequest, false).blockingFirst();
+ assertTrue(((List>)paramCaptor.getValue().getTools()).isEmpty());
+ assertNull(paramCaptor.getValue().getToolChoice(), "No toolChoice set with empty tools");
+ }
+ @Test
+ @Tag("valid")
+ public void testGenerateContentWithFunctionCallPart() {
+ FunctionCall functionCall = FunctionCall.builder().id("fc42").name("functionX").args(ImmutableMap.of("foo", "bar")).build();
+ Part part = Part.builder().functionCall(functionCall).build();
+ Content content = Content.builder().role("user").parts(ImmutableList.of(part)).build();
+ LlmRequest llmRequest = mock(LlmRequest.class);
+ when(llmRequest.contents()).thenReturn(ImmutableList.of(content));
+ when(llmRequest.config()).thenReturn(Optional.empty());
+ when(llmRequest.model()).thenReturn(Optional.of(DEFAULT_MODEL));
+ when(llmRequest.tools()).thenReturn(Collections.emptyMap());
+ ContentBlock cbFunction = ContentBlock.ofToolUse("fc42", "functionX", com.anthropic.core.JsonValue.from(ImmutableMap.of("foo", "bar")));
+ Message message = Message.builder().content(ImmutableList.of(cbFunction)).build();
+ when(mockMessages.create(any())).thenReturn(message);
+ LlmResponse actual = claude.generateContent(llmRequest, false).blockingFirst();
+ List parts = (List) actual.content().parts().orElse(ImmutableList.of());
+ assertEquals(1, parts.size());
+ assertTrue(parts.get(0).functionCall().isPresent(), "FunctionCall should map to ToolUseBlockParam");
+ assertEquals("functionX", parts.get(0).functionCall().get().name().get());
+ }
+ @Test
+ @Tag("valid")
+ public void testGenerateContentWithFunctionResponsePart() {
+ Map responseMap = new HashMap<>();
+ responseMap.put("result", "the-result");
+ Part.FunctionResponse functionResponse = Part.FunctionResponse.builder().id("rid").response(responseMap).build();
+ Part part = Part.builder().functionResponse(functionResponse).build();
+ Content content = Content.builder().role("user").parts(ImmutableList.of(part)).build();
+ LlmRequest llmRequest = mock(LlmRequest.class);
+ when(llmRequest.contents()).thenReturn(ImmutableList.of(content));
+ when(llmRequest.config()).thenReturn(Optional.empty());
+ when(llmRequest.model()).thenReturn(Optional.of(DEFAULT_MODEL));
+ when(llmRequest.tools()).thenReturn(Collections.emptyMap());
+ ContentBlock cbResult = ContentBlock.ofToolResult("rid", "the-result", false);
+ Message message = Message.builder().content(ImmutableList.of(cbResult)).build();
+ when(mockMessages.create(any())).thenReturn(message);
+ LlmResponse actual = claude.generateContent(llmRequest, false).blockingFirst();
+ List parts = (List) actual.content().parts().orElse(ImmutableList.of());
+ assertEquals(1, parts.size());
+ assertFalse(parts.get(0).text().isPresent());
+ assertFalse(parts.get(0).functionCall().isPresent());
+ }
+ @Test
+ @Tag("integration")
+ public void testGenerateContentWithMixedPartTypes() {
+ Part textPart = Part.builder().text("Alpha").build();
+ FunctionCall fnCall = FunctionCall.builder().id("id1").name("toolZ").args(ImmutableMap.of("a", "b")).build();
+ Part fnCallPart = Part.builder().functionCall(fnCall).build();
+ Map fnRespMap = new HashMap<>();
+ fnRespMap.put("result", "done");
+ Part.FunctionResponse fnResp = Part.FunctionResponse.builder().id("rid2").response(fnRespMap).build();
+ Part fnRespPart = Part.builder().functionResponse(fnResp).build();
+ Content content = Content.builder().role("user").parts(ImmutableList.of(textPart, fnCallPart, fnRespPart)).build();
+ LlmRequest llmRequest = mock(LlmRequest.class);
+ when(llmRequest.contents()).thenReturn(ImmutableList.of(content));
+ when(llmRequest.config()).thenReturn(Optional.empty());
+ when(llmRequest.model()).thenReturn(Optional.of(DEFAULT_MODEL));
+ when(llmRequest.tools()).thenReturn(Collections.emptyMap());
+ ContentBlock cb1 = ContentBlock.ofText("Alpha");
+ ContentBlock cb2 = ContentBlock.ofToolUse("id1", "toolZ", com.anthropic.core.JsonValue.from(ImmutableMap.of("a", "b")));
+ ContentBlock cb3 = ContentBlock.ofToolResult("rid2", "done", false);
+ Message message = Message.builder().content(ImmutableList.of(cb1, cb2, cb3)).build();
+ when(mockMessages.create(any())).thenReturn(message);
+ LlmResponse resp = claude.generateContent(llmRequest, false).blockingFirst();
+ List parts = (List) resp.content().parts().orElse(ImmutableList.of());
+ assertEquals(3, parts.size());
+ assertEquals("Alpha", parts.get(0).text().orElse(""));
+ assertTrue(parts.get(1).functionCall().isPresent());
+ assertFalse(parts.get(2).text().isPresent());
+ }
+ @Test
+ @Tag("boundary")
+ public void testGenerateContentRespectsMaxTokens() {
+ int maxOverride = 5123; // TODO: Update to desired max tokens value if needed
+ claude = new Claude(DEFAULT_MODEL, anthropicClient, maxOverride);
+ Content content = Content.builder().role("user").parts(ImmutableList.of(Part.builder().text("check tokens").build())).build();
+ LlmRequest llmRequest = mock(LlmRequest.class);
+ when(llmRequest.contents()).thenReturn(ImmutableList.of(content));
+ when(llmRequest.config()).thenReturn(Optional.empty());
+ when(llmRequest.model()).thenReturn(Optional.of(DEFAULT_MODEL));
+ when(llmRequest.tools()).thenReturn(Collections.emptyMap());
+ Message message = Message.builder().content(ImmutableList.of(ContentBlock.ofText("maxTokens ok"))).build();
+ ArgumentCaptor captor = ArgumentCaptor.forClass(MessageCreateParams.class);
+ when(mockMessages.create(captor.capture())).thenReturn(message);
+ claude.generateContent(llmRequest, false).blockingFirst();
+ assertEquals(maxOverride, ((Integer)captor.getValue().getMaxTokens()).intValue(), "Should respect Claude.maxTokens override");
+ }
+ @Test
+ @Tag("boundary")
+ public void testGenerateContentWithEmptyContentsList() {
+ LlmRequest llmRequest = mock(LlmRequest.class);
+ when(llmRequest.contents()).thenReturn(ImmutableList.of());
+ when(llmRequest.config()).thenReturn(Optional.empty());
+ when(llmRequest.model()).thenReturn(Optional.of(DEFAULT_MODEL));
+ when(llmRequest.tools()).thenReturn(Collections.emptyMap());
+ Message message = Message.builder().content(ImmutableList.of()).build();
+ when(mockMessages.create(any())).thenReturn(message);
+ LlmResponse resp = claude.generateContent(llmRequest, false).blockingFirst();
+ assertNotNull(resp);
+ assertNull(resp.content(), "Content of response should be null if contents empty");
+ }
+ @Test
+ @Tag("valid")
+ public void testGenerateContentWithStreamTrueParameter() {
+ Content content = Content.builder().role("user").parts(ImmutableList.of(Part.builder().text("stream?").build())).build();
+ LlmRequest llmRequest = mock(LlmRequest.class);
+ when(llmRequest.contents()).thenReturn(ImmutableList.of(content));
+ when(llmRequest.config()).thenReturn(Optional.empty());
+ when(llmRequest.model()).thenReturn(Optional.of(DEFAULT_MODEL));
+ when(llmRequest.tools()).thenReturn(Collections.emptyMap());
+ Message message = Message.builder().content(ImmutableList.of(ContentBlock.ofText("yes stream param"))).build();
+ when(mockMessages.create(any())).thenReturn(message);
+ LlmResponse resultStream = claude.generateContent(llmRequest, true).blockingFirst();
+ LlmResponse resultNonStream = claude.generateContent(llmRequest, false).blockingFirst();
+ // Both results should be same
+ assertEquals(
+ ((List)resultNonStream.content().parts().get()).get(0).text().get(),
+ ((List)resultStream.content().parts().get()).get(0).text().get(),
+ "Result should be same regardless of stream param"
+ );
+ }
+ @Test
+ @Tag("boundary")
+ public void testGenerateContentHandlesNullMessage() {
+ Content content = Content.builder().role("user").parts(ImmutableList.of(Part.builder().text("null message").build())).build();
+ LlmRequest llmRequest = mock(LlmRequest.class);
+ when(llmRequest.contents()).thenReturn(ImmutableList.of(content));
+ when(llmRequest.config()).thenReturn(Optional.empty());
+ when(llmRequest.model()).thenReturn(Optional.of(DEFAULT_MODEL));
+ when(llmRequest.tools()).thenReturn(Collections.emptyMap());
+ when(mockMessages.create(any())).thenReturn(null);
+ LlmResponse resp = claude.generateContent(llmRequest, false).blockingFirst();
+ assertNotNull(resp, "generateContent should handle null from client and return non-null LlmResponse");
+ }
+ @Test
+ @Tag("invalid")
+ public void testGenerateContentThrowsOnUnsupportedPart() {
+ Part unsupported = Part.builder()
+ // No text, functionCall or functionResponse
+ .build();
+ Content content = Content.builder().role("user").parts(ImmutableList.of(unsupported)).build();
+ LlmRequest llmRequest = mock(LlmRequest.class);
+ when(llmRequest.contents()).thenReturn(ImmutableList.of(content));
+ when(llmRequest.config()).thenReturn(Optional.empty());
+ when(llmRequest.model()).thenReturn(Optional.of(DEFAULT_MODEL));
+ when(llmRequest.tools()).thenReturn(Collections.emptyMap());
+ assertThrows(UnsupportedOperationException.class, () -> {
+ claude.generateContent(llmRequest, false).blockingFirst();
+ }, "Should throw on unsupported part structure");
+ }
+}
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 6009c7316..b21618b54 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,4 +1,4 @@
-
+
-
+
4.0.0
-
com.google.adk
google-adk-parent
- 0.4.1-SNAPSHOT
+ 0.4.1-SNAPSHOT
+
pom
-
Google Agent Development Kit Maven Parent POM
https://github.com/google/adk-java
Google Agent Development Kit (ADK) for Java
-
core
dev
@@ -39,12 +35,10 @@
a2a
a2a/webservice
-
17
${java.version}
UTF-8
-
1.11.0
3.4.1
1.49.0
@@ -73,7 +67,6 @@
3.9.0
5.4.3
-
@@ -112,7 +105,6 @@
pom
import
-
com.anthropic
@@ -274,9 +266,21 @@
assertj-core
${assertj.version}
+
+ org.mockito
+ mockito-junit-jupiter
+ 5.2.0
+ test
+
+
+
+ io.spring.javaformat
+ spring-javaformat-formatter
+ 0.0.40
+
+
-
@@ -324,8 +328,7 @@
plain
-
+
**/*Test.java
@@ -469,6 +472,36 @@
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.2.5
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-report-plugin
+ 3.2.5
+
+ testReport
+
+
+
+
+ org.apache.maven.plugins
+ maven-site-plugin
+ 2.1
+
+ testReport
+
+
+
+
+ io.spring.javaformat
+ spring-javaformat-maven-plugin
+ 0.0.40
+
+
@@ -528,7 +561,6 @@
-
The Apache License, Version 2.0
@@ -558,4 +590,19 @@
https://central.sonatype.com/repository/maven-snapshots/
+
+
+ org.mockito
+ mockito-junit-jupiter
+ 5.2.0
+ test
+
+
+
+ io.spring.javaformat
+ spring-javaformat-formatter
+ 0.0.40
+
+
+
\ No newline at end of file