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
21 changes: 13 additions & 8 deletions core/pom.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0"?>
<!--
Copyright 2025 Google LLC

Expand All @@ -16,19 +16,15 @@
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.google.adk</groupId>
<artifactId>google-adk-parent</artifactId>
<version>0.4.1-SNAPSHOT</version><!-- {x-version-update:google-adk:current} -->
<version>0.4.1-SNAPSHOT</version>
<!-- {x-version-update:google-adk:current} -->
</parent>

<artifactId>google-adk</artifactId>
<name>Agent Development Kit</name>
<description>Agent Development Kit: an open-source, code-first toolkit designed to simplify building, evaluating, and deploying advanced AI agents anywhere.</description>



<dependencies>
<dependency>
<groupId>com.anthropic</groupId>
Expand Down Expand Up @@ -201,6 +197,15 @@
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>io.spring.javaformat</groupId>
<artifactId>spring-javaformat-maven-plugin</artifactId>
<version>0.0.40</version>
<!--Plugin added by RoostGPT-->
</plugin>
</plugins>
<pluginManagement>
<plugins/>
</pluginManagement>
</build>
</project>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,340 @@
/*
* 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-5

ROOST_METHOD_HASH=generateContent_bc4a182290
ROOST_METHOD_SIG_HASH=generateContent_c08b9769fe

Scenario 1: Basic request with a single user text message, no tools, no system instruction, using default model and default maxTokens

Details:
TestName: basicUserTextNoToolsUsesDefaults
Description: This test verifies that generateContent builds MessageCreateParams with the expected defaults when the request has one user text Content, no tools, no system instruction, and no model override. It ensures model falls back to the Claude instance’s model(), maxTokens uses the default (8192), no toolChoice is set, and the messages list includes the user text with Role.USER.

Execution:
Arrange: Prepare a mock AnthropicClient with messages().create(...) returning a Message containing a simple text ContentBlock. Construct a Claude instance with a model name and the mock client. Build an LlmRequest mock where contents() returns a single Content with a text Part and role “user”, config() is empty, tools() is empty, and model() is empty.
Act: Call generateContent(llmRequest, false).
Assert: Verify messages().create(...) is invoked exactly once and capture the MessageCreateParams to confirm it contains: the instance model(), system is an empty string, maxTokens = 8192, messages list with one element having Role.USER and the provided text, and no toolChoice or tools configured. Verify the returned Flowable emits exactly one LlmResponse and completes.

Validation:
Ensures correct defaulting behavior: no system text, no tools when none requested, default model fallback, and default maxTokens usage. Confirms the method produces a single response event and completes successfully in the basic scenario.

*/

// ********RoostGPT********

package com.google.adk.models;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.anthropic.client.AnthropicClient;
import com.anthropic.models.messages.Message;
import com.anthropic.models.messages.MessageCreateParams;
import com.anthropic.models.messages.MessageParam;
import com.anthropic.models.messages.MessageParam.Role;
import com.anthropic.models.messages.ToolChoice;
import com.anthropic.models.messages.ToolUnion;
import com.google.adk.tools.BaseTool;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.genai.types.Content;
import com.google.genai.types.GenerateContentConfig;
import com.google.genai.types.Part;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.subscribers.TestSubscriber;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Answers;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
public abstract class ClaudeGenerateContentTest extends BaseLlm {

public ClaudeGenerateContentTest() {
super("test-model");
}

@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private AnthropicClient anthropicClient;

@InjectMocks
private Claude unusedClaudeForInjection = new Claude("test-model", anthropicClient); // Not

// used
// directly
// in
// tests

@Test
@Tag("valid")
public void testBasicUserTextNoToolsUsesDefaults() {
// Arrange
String modelName = "claude-3-opus"; // TODO: Change model name if needed
Claude claude = new Claude(modelName, anthropicClient);
Content userContent =
Content.builder()
.role("user")
.parts(ImmutableList.of(Part.builder().text("Hello, Claude!").build()))
.build();
LlmRequest llmRequest = LlmRequest.builder().contents(ImmutableList.of(userContent)).build();
Message mockMessage = Mockito.mock(Message.class);
when(mockMessage.content()).thenReturn(ImmutableList.of());
when(anthropicClient.messages().create(any(MessageCreateParams.class))).thenReturn(mockMessage);
// Act
Flowable<LlmResponse> flowable = claude.generateContent(llmRequest, false);
TestSubscriber<LlmResponse> ts = flowable.test();
// Assert output Flowable behavior
ts.assertComplete();
ts.assertValueCount(1);
List<LlmResponse> values = ts.values();
assertEquals(1, (int) values.size());
assertNotNull(values.get(0));
// Assert request sent to Anthropic
ArgumentCaptor<MessageCreateParams> paramsCaptor =
ArgumentCaptor.forClass(MessageCreateParams.class);
verify(anthropicClient.messages(), times(1)).create(paramsCaptor.capture());
MessageCreateParams params = paramsCaptor.getValue();
assertEquals(claude.model(), params.model());
assertEquals("", params.system());
assertEquals(8192, (int) params.maxTokens());
List<MessageParam> sentMessages = params.messages();
assertNotNull(sentMessages);
assertEquals(1, (int) sentMessages.size());
MessageParam mp = sentMessages.get(0);
assertEquals(Role.USER, mp.role());
assertNotNull(mp.content());
// Tool choice and tools should not be set when no tools provided
Optional<ToolChoice> toolChoiceOpt = params.toolChoice();
assertTrue(toolChoiceOpt.isEmpty());
Optional<List<ToolUnion>> toolsListOpt = params.tools();
assertTrue(toolsListOpt.isEmpty() || toolsListOpt.get().isEmpty());
}

@Test
@Tag("boundary")
public void testSystemInstructionJoinsMultipleTextParts() {
// Arrange
String modelName = "claude-3-sonnet"; // TODO: Change model name if needed
Claude claude = new Claude(modelName, anthropicClient);
Content userContent =
Content.builder()
.role("user")
.parts(ImmutableList.of(Part.builder().text("Run task").build()))
.build();
Content systemInstruction =
Content.builder()
.parts(
ImmutableList.of(
Part.builder().text("You are a helpful assistant.").build(),
Part.builder().text("Answer concisely.").build()))
.build();
GenerateContentConfig mockConfig = Mockito.mock(GenerateContentConfig.class);
when(mockConfig.systemInstruction()).thenReturn(Optional.of(systemInstruction));
when(mockConfig.tools()).thenReturn(Optional.empty());
// Mock LlmRequest directly to avoid builder signature mismatch for config
LlmRequest llmRequest = Mockito.mock(LlmRequest.class);
when(llmRequest.contents()).thenReturn(ImmutableList.of(userContent));
when(llmRequest.config()).thenReturn(Optional.of(mockConfig));
when(llmRequest.tools()).thenReturn(ImmutableMap.of());
when(llmRequest.model()).thenReturn(Optional.empty());
Message mockMessage = Mockito.mock(Message.class);
when(mockMessage.content()).thenReturn(ImmutableList.of());
when(anthropicClient.messages().create(any(MessageCreateParams.class))).thenReturn(mockMessage);
// Act
Flowable<LlmResponse> flowable = claude.generateContent(llmRequest, false);
TestSubscriber<LlmResponse> ts = flowable.test();
// Assert Flowable behavior
ts.assertComplete();
ts.assertValueCount(1);
assertNotNull(ts.values().get(0));
// Assert params
ArgumentCaptor<MessageCreateParams> paramsCaptor =
ArgumentCaptor.forClass(MessageCreateParams.class);
verify(anthropicClient.messages(), times(1)).create(paramsCaptor.capture());
MessageCreateParams params = paramsCaptor.getValue();
assertEquals(claude.model(), params.model());
assertEquals("You are a helpful assistant.\nAnswer concisely.", params.system());
assertEquals(8192, (int) params.maxTokens());
}

@Test
@Tag("valid")
public void testNonEmptyToolsMapSetsToolChoiceAuto() {
// Arrange
String modelName = "claude-3-haiku"; // TODO: Change model name if needed
Claude claude = new Claude(modelName, anthropicClient);
BaseTool dummyTool = Mockito.mock(BaseTool.class);
Content userContent =
Content.builder()
.role("user")
.parts(ImmutableList.of(Part.builder().text("Compute sum").build()))
.build();
LlmRequest llmRequest =
LlmRequest.builder()
.contents(ImmutableList.of(userContent))
.tools(ImmutableMap.of("dummy", dummyTool))
.build();
Message mockMessage = Mockito.mock(Message.class);
when(mockMessage.content()).thenReturn(ImmutableList.of());
when(anthropicClient.messages().create(any(MessageCreateParams.class))).thenReturn(mockMessage);
// Act
Flowable<LlmResponse> flowable = claude.generateContent(llmRequest, false);
TestSubscriber<LlmResponse> ts = flowable.test();
// Assert Flowable behavior
ts.assertComplete();
ts.assertValueCount(1);
assertNotNull(ts.values().get(0));
// Verify params sent
ArgumentCaptor<MessageCreateParams> paramsCaptor =
ArgumentCaptor.forClass(MessageCreateParams.class);
verify(anthropicClient.messages(), times(1)).create(paramsCaptor.capture());
MessageCreateParams params = paramsCaptor.getValue();
assertEquals(claude.model(), params.model());
assertEquals(8192, (int) params.maxTokens());
Optional<ToolChoice> toolChoiceOpt = params.toolChoice();
assertTrue(toolChoiceOpt.isPresent());
Optional<List<ToolUnion>> toolsListOpt = params.tools();
assertTrue(toolsListOpt.isEmpty() || toolsListOpt.get().isEmpty());
}

@Test
@Tag("boundary")
public void testModelOverrideFromRequest() {
// Arrange
String defaultModelName = "claude-3-opus";
Claude claude = new Claude(defaultModelName, anthropicClient);
String overrideModel = "override-model";
Content userContent =
Content.builder()
.role("user")
.parts(ImmutableList.of(Part.builder().text("Ping").build()))
.build();
// Mock LlmRequest to provide model override
LlmRequest llmRequestWithModel = Mockito.mock(LlmRequest.class);
when(llmRequestWithModel.contents()).thenReturn(ImmutableList.of(userContent));
when(llmRequestWithModel.model()).thenReturn(Optional.of(overrideModel));
when(llmRequestWithModel.config()).thenReturn(Optional.empty());
when(llmRequestWithModel.tools()).thenReturn(ImmutableMap.of());
Message mockMessage = Mockito.mock(Message.class);
when(mockMessage.content()).thenReturn(ImmutableList.of());
when(anthropicClient.messages().create(any(MessageCreateParams.class))).thenReturn(mockMessage);
// Act
Flowable<LlmResponse> flowable = claude.generateContent(llmRequestWithModel, false);
TestSubscriber<LlmResponse> ts = flowable.test();
// Assert
ts.assertComplete();
ts.assertValueCount(1);
ArgumentCaptor<MessageCreateParams> paramsCaptor =
ArgumentCaptor.forClass(MessageCreateParams.class);
verify(anthropicClient.messages(), times(1)).create(paramsCaptor.capture());
MessageCreateParams params = paramsCaptor.getValue();
assertEquals(overrideModel, params.model());
assertEquals(8192, (int) params.maxTokens());
}

@Test
@Tag("boundary")
public void testCustomMaxTokensFromConstructor() {
// Arrange
int customMaxTokens = 1024;
Claude claude = new Claude("claude-3-haiku", anthropicClient, customMaxTokens);
Content userContent =
Content.builder()
.role("user")
.parts(ImmutableList.of(Part.builder().text("Short response please").build()))
.build();
LlmRequest llmRequest = LlmRequest.builder().contents(ImmutableList.of(userContent)).build();
Message mockMessage = Mockito.mock(Message.class);
when(mockMessage.content()).thenReturn(ImmutableList.of());
when(anthropicClient.messages().create(any(MessageCreateParams.class))).thenReturn(mockMessage);
// Act
Flowable<LlmResponse> flowable = claude.generateContent(llmRequest, false);
TestSubscriber<LlmResponse> ts = flowable.test();
// Assert
ts.assertComplete();
ts.assertValueCount(1);
ArgumentCaptor<MessageCreateParams> paramsCaptor =
ArgumentCaptor.forClass(MessageCreateParams.class);
verify(anthropicClient.messages(), times(1)).create(paramsCaptor.capture());
MessageCreateParams params = paramsCaptor.getValue();
assertEquals(customMaxTokens, (int) params.maxTokens());
}

@Test
@Tag("invalid")
public void testUnsupportedPartThrowsException() {
// Arrange
Claude claude = new Claude("claude-3-opus", anthropicClient);
// Create a Part without text, functionCall or functionResponse to trigger
// UnsupportedOperationException
Part unsupportedPart = Part.builder().build();
Content userContent =
Content.builder().role("user").parts(ImmutableList.of(unsupportedPart)).build();
LlmRequest llmRequest = LlmRequest.builder().contents(ImmutableList.of(userContent)).build();
// Act + Assert
assertThrows(
UnsupportedOperationException.class,
() -> {
claude.generateContent(llmRequest, false);
});
}

@Test
@Tag("valid")
public void testAssistantRoleMapsToAssistant() {
// Arrange
Claude claude = new Claude("claude-3-opus", anthropicClient);
Content assistantContent =
Content.builder()
.role("assistant")
.parts(ImmutableList.of(Part.builder().text("Assistant message").build()))
.build();
LlmRequest llmRequest =
LlmRequest.builder().contents(ImmutableList.of(assistantContent)).build();
Message mockMessage = Mockito.mock(Message.class);
when(mockMessage.content()).thenReturn(ImmutableList.of());
when(anthropicClient.messages().create(any(MessageCreateParams.class))).thenReturn(mockMessage);
// Act
Flowable<LlmResponse> flowable = claude.generateContent(llmRequest, false);
TestSubscriber<LlmResponse> ts = flowable.test();
// Assert
ts.assertComplete();
ts.assertValueCount(1);
ArgumentCaptor<MessageCreateParams> paramsCaptor =
ArgumentCaptor.forClass(MessageCreateParams.class);
verify(anthropicClient.messages(), times(1)).create(paramsCaptor.capture());
MessageCreateParams params = paramsCaptor.getValue();
List<MessageParam> sentMessages = params.messages();
assertNotNull(sentMessages);
assertEquals(1, (int) sentMessages.size());
assertEquals(Role.ASSISTANT, sentMessages.get(0).role());
assertNotNull(sentMessages.get(0).content());
}
}
Loading