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.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
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.ContentBlock;
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.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 java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.DisplayName;
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.junit.jupiter.MockitoExtension;

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

// TODO: Provide a valid model name if needed for other tests
private static final String DEFAULT_MODEL = "test-model";

public ClaudeGenerateContentTest() {
super(DEFAULT_MODEL);
}

@Test
@DisplayName(
"Scenario 1: Basic request with single user text, no tools, no system instruction - uses defaults")
@Tag("valid")
public void testBasicUserTextNoToolsUsesDefaults() {
AnthropicClient anthropicClient =
org.mockito.Mockito.mock(AnthropicClient.class, RETURNS_DEEP_STUBS);
// Mock Anthropic response: a Message containing a single text ContentBlock
Message message = org.mockito.Mockito.mock(Message.class);
ContentBlock textBlock = org.mockito.Mockito.mock(ContentBlock.class, RETURNS_DEEP_STUBS);
when(textBlock.isText()).thenReturn(true);
when(textBlock.asText().text()).thenReturn("ok");
when(message.content()).thenReturn(ImmutableList.of(textBlock));
ArgumentCaptor<MessageCreateParams> paramsCaptor =
ArgumentCaptor.forClass(MessageCreateParams.class);
when(anthropicClient.messages().create(paramsCaptor.capture())).thenReturn(message);
// Build request with one user text content, no tools, no system, no model
// override
Part userText = Part.builder().text("hello").build();
Content userContent = Content.builder().role("user").parts(ImmutableList.of(userText)).build();
LlmRequest llmRequest = org.mockito.Mockito.mock(LlmRequest.class);
when(llmRequest.contents()).thenReturn(ImmutableList.of(userContent));
when(llmRequest.config()).thenReturn(Optional.empty());
when(llmRequest.tools()).thenReturn(ImmutableMap.of());
when(llmRequest.model()).thenReturn(Optional.empty());
Claude claude = new Claude(DEFAULT_MODEL, anthropicClient);
Flowable<LlmResponse> flowable = claude.generateContent(llmRequest, false);
// Verify anthropic messages().create called once and capture parameters
verify(anthropicClient.messages(), times(1)).create(any(MessageCreateParams.class));
MessageCreateParams captured = paramsCaptor.getValue();
assertNotNull((Object) captured);
// Verify model falls back to Claude instance model
assertEquals((Object) DEFAULT_MODEL, (Object) captured.model());
// Verify system text is empty string when not provided
assertEquals((Object) "", (Object) captured.system());
// Verify default maxTokens 8192
assertEquals((Object) Integer.valueOf(8192), (Object) captured.maxTokens());
// Verify no toolChoice set and tools are not configured by default
// Depending on underlying SDK, tools() may be Optional.empty() or null/empty when
// not set
try {
assertNull((Object) captured.toolChoice());
} catch (Throwable ignored) {
// In case SDK represents absence differently, no-op
}
try {
Object maybeTools = (Object) captured.tools();
if (maybeTools instanceof java.util.Optional) {
java.util.Optional<?> toolsOpt = (java.util.Optional<?>) maybeTools;
assertEquals((Object) Boolean.TRUE, (Object) Boolean.valueOf(toolsOpt.isEmpty()));
} else if (maybeTools instanceof java.util.List) {
java.util.List<?> toolsList = (java.util.List<?>) maybeTools;
assertEquals((Object) Integer.valueOf(0), (Object) Integer.valueOf(toolsList.size()));
}
} catch (Throwable ignored) {
// In case SDK represents absence differently, no-op
}
// Verify messages list includes the user text with Role.USER
List<MessageParam> msgParams = captured.messages();
assertNotNull((Object) msgParams);
assertEquals((Object) 1, (Object) Integer.valueOf(msgParams.size()));
MessageParam first = msgParams.get(0);
assertEquals((Object) Role.USER, (Object) first.role());
// Assert Flowable emits exactly one LlmResponse and completes
List<LlmResponse> out = flowable.toList().blockingGet();
assertNotNull((Object) out);
assertEquals((Object) 1, (Object) Integer.valueOf(out.size()));
assertNotNull((Object) out.get(0));
}

@Test
@DisplayName("Non-empty tools map results in toolChoice being set and sent to Anthropic")
@Tag("valid")
public void testNonEmptyToolsSetsToolChoice() {
AnthropicClient anthropicClient =
org.mockito.Mockito.mock(AnthropicClient.class, RETURNS_DEEP_STUBS);
Message message = org.mockito.Mockito.mock(Message.class);
when(message.content()).thenReturn(Collections.emptyList());
ArgumentCaptor<MessageCreateParams> paramsCaptor =
ArgumentCaptor.forClass(MessageCreateParams.class);
when(anthropicClient.messages().create(paramsCaptor.capture())).thenReturn(message);
Part userText = Part.builder().text("with tools").build();
Content userContent = Content.builder().role("user").parts(ImmutableList.of(userText)).build();
LlmRequest llmRequest = org.mockito.Mockito.mock(LlmRequest.class);
when(llmRequest.contents()).thenReturn(ImmutableList.of(userContent));
when(llmRequest.config()).thenReturn(Optional.empty());
@SuppressWarnings({"unchecked", "rawtypes"})
Map toolsMap = ImmutableMap.of("dummy", new Object());
when(llmRequest.tools()).thenReturn(toolsMap);
when(llmRequest.model()).thenReturn(Optional.empty());
Claude claude = new Claude(DEFAULT_MODEL, anthropicClient);
List<LlmResponse> out = claude.generateContent(llmRequest, false).toList().blockingGet();
assertEquals((Object) 1, (Object) Integer.valueOf(out.size()));
MessageCreateParams captured = paramsCaptor.getValue();
assertNotNull((Object) captured);
// toolChoice should be set when tools map is non-empty
try {
Object toolChoice = captured.toolChoice();
if (toolChoice instanceof java.util.Optional) {
assertEquals(
(Object) Boolean.FALSE,
(Object) Boolean.valueOf(((java.util.Optional<?>) toolChoice).isEmpty()));
} else {
assertNotNull((Object) toolChoice);
}
} catch (Throwable ignored) {
// If underlying SDK exposes differently, skip strict assertion
}
}

@Test
@DisplayName("System instruction text is extracted and joined from config")
@Tag("valid")
public void testSystemInstructionExtractedFromConfig() {
AnthropicClient anthropicClient =
org.mockito.Mockito.mock(AnthropicClient.class, RETURNS_DEEP_STUBS);
Message message = org.mockito.Mockito.mock(Message.class);
when(message.content()).thenReturn(Collections.emptyList());
ArgumentCaptor<MessageCreateParams> paramsCaptor =
ArgumentCaptor.forClass(MessageCreateParams.class);
when(anthropicClient.messages().create(paramsCaptor.capture())).thenReturn(message);
// Build system instruction content with multiple text parts
Part s1 = Part.builder().text("line1").build();
Part s2 = Part.builder().text("line2").build();
Content sysContent = Content.builder().role("system").parts(ImmutableList.of(s1, s2)).build();
GenerateContentConfig config = org.mockito.Mockito.mock(GenerateContentConfig.class);
when(config.systemInstruction()).thenReturn(Optional.of(sysContent));
when(config.tools()).thenReturn(Optional.empty());
Part userText = Part.builder().text("hello").build();
Content userContent = Content.builder().role("user").parts(ImmutableList.of(userText)).build();
LlmRequest llmRequest = org.mockito.Mockito.mock(LlmRequest.class);
when(llmRequest.contents()).thenReturn(ImmutableList.of(userContent));
when(llmRequest.config()).thenReturn(Optional.of(config));
when(llmRequest.tools()).thenReturn(ImmutableMap.of());
when(llmRequest.model()).thenReturn(Optional.empty());
Claude claude = new Claude(DEFAULT_MODEL, anthropicClient);
List<LlmResponse> out = claude.generateContent(llmRequest, false).toList().blockingGet();
assertEquals((Object) 1, (Object) Integer.valueOf(out.size()));
MessageCreateParams captured = paramsCaptor.getValue();
assertNotNull((Object) captured);
assertEquals((Object) "line1\nline2", (Object) captured.system());
}

@Test
@DisplayName("Model override on request is used instead of instance default")
@Tag("boundary")
public void testModelOverrideUsedWhenProvided() {
AnthropicClient anthropicClient =
org.mockito.Mockito.mock(AnthropicClient.class, RETURNS_DEEP_STUBS);
Message message = org.mockito.Mockito.mock(Message.class);
when(message.content()).thenReturn(Collections.emptyList());
ArgumentCaptor<MessageCreateParams> paramsCaptor =
ArgumentCaptor.forClass(MessageCreateParams.class);
when(anthropicClient.messages().create(paramsCaptor.capture())).thenReturn(message);
Part userText = Part.builder().text("hi").build();
Content userContent = Content.builder().role("user").parts(ImmutableList.of(userText)).build();
String overrideModel = "override-model"; // TODO: Change if a specific model is
// required
LlmRequest llmRequest = org.mockito.Mockito.mock(LlmRequest.class);
when(llmRequest.contents()).thenReturn(ImmutableList.of(userContent));
when(llmRequest.config()).thenReturn(Optional.empty());
when(llmRequest.tools()).thenReturn(ImmutableMap.of());
when(llmRequest.model()).thenReturn(Optional.of(overrideModel));
Claude claude = new Claude(DEFAULT_MODEL, anthropicClient);
List<LlmResponse> out = claude.generateContent(llmRequest, false).toList().blockingGet();
assertEquals((Object) 1, (Object) Integer.valueOf(out.size()));
MessageCreateParams captured = paramsCaptor.getValue();
assertNotNull((Object) captured);
assertEquals((Object) overrideModel, (Object) captured.model());
}

@Test
@DisplayName("Custom maxTokens passed via constructor is respected")
@Tag("boundary")
public void testCustomMaxTokensRespected() {
AnthropicClient anthropicClient =
org.mockito.Mockito.mock(AnthropicClient.class, RETURNS_DEEP_STUBS);
Message message = org.mockito.Mockito.mock(Message.class);
when(message.content()).thenReturn(Collections.emptyList());
ArgumentCaptor<MessageCreateParams> paramsCaptor =
ArgumentCaptor.forClass(MessageCreateParams.class);
when(anthropicClient.messages().create(paramsCaptor.capture())).thenReturn(message);
Part userText = Part.builder().text("hi").build();
Content userContent = Content.builder().role("user").parts(ImmutableList.of(userText)).build();
LlmRequest llmRequest = org.mockito.Mockito.mock(LlmRequest.class);
when(llmRequest.contents()).thenReturn(ImmutableList.of(userContent));
when(llmRequest.config()).thenReturn(Optional.empty());
when(llmRequest.tools()).thenReturn(ImmutableMap.of());
when(llmRequest.model()).thenReturn(Optional.empty());
int customMax = 1234; // TODO: Adjust based on test requirements
Claude claude = new Claude(DEFAULT_MODEL, anthropicClient, customMax);
List<LlmResponse> out = claude.generateContent(llmRequest, false).toList().blockingGet();
assertEquals((Object) 1, (Object) Integer.valueOf(out.size()));
MessageCreateParams captured = paramsCaptor.getValue();
assertNotNull((Object) captured);
assertEquals((Object) Integer.valueOf(customMax), (Object) captured.maxTokens());
}

@Test
@DisplayName("Assistant role maps to Role.ASSISTANT")
@Tag("valid")
public void testAssistantRoleMapping() {
AnthropicClient anthropicClient =
org.mockito.Mockito.mock(AnthropicClient.class, RETURNS_DEEP_STUBS);
Message message = org.mockito.Mockito.mock(Message.class);
when(message.content()).thenReturn(Collections.emptyList());
ArgumentCaptor<MessageCreateParams> paramsCaptor =
ArgumentCaptor.forClass(MessageCreateParams.class);
when(anthropicClient.messages().create(paramsCaptor.capture())).thenReturn(message);
Part assistantText = Part.builder().text("assistant text").build();
Content assistantContent =
Content.builder().role("assistant").parts(ImmutableList.of(assistantText)).build();
LlmRequest llmRequest = org.mockito.Mockito.mock(LlmRequest.class);
when(llmRequest.contents()).thenReturn(ImmutableList.of(assistantContent));
when(llmRequest.config()).thenReturn(Optional.empty());
when(llmRequest.tools()).thenReturn(ImmutableMap.of());
when(llmRequest.model()).thenReturn(Optional.empty());
Claude claude = new Claude(DEFAULT_MODEL, anthropicClient);
List<LlmResponse> out = claude.generateContent(llmRequest, false).toList().blockingGet();
assertEquals((Object) 1, (Object) Integer.valueOf(out.size()));
MessageCreateParams captured = paramsCaptor.getValue();
List<MessageParam> msgParams = captured.messages();
assertNotNull((Object) msgParams);
assertEquals((Object) 1, (Object) Integer.valueOf(msgParams.size()));
assertEquals((Object) Role.ASSISTANT, (Object) msgParams.get(0).role());
}

@Test
@DisplayName("Unsupported Part type triggers UnsupportedOperationException and no Anthropic call")
@Tag("invalid")
public void testUnsupportedPartTypeThrows() {
AnthropicClient anthropicClient =
org.mockito.Mockito.mock(AnthropicClient.class, RETURNS_DEEP_STUBS);
// Part with no text/functionCall/functionResponse to trigger
// UnsupportedOperationException
Part invalidPart = Part.builder().build();
Content badContent =
Content.builder().role("user").parts(ImmutableList.of(invalidPart)).build();
LlmRequest llmRequest = org.mockito.Mockito.mock(LlmRequest.class);
when(llmRequest.contents()).thenReturn(ImmutableList.of(badContent));
when(llmRequest.config()).thenReturn(Optional.empty());
when(llmRequest.tools()).thenReturn(ImmutableMap.of());
when(llmRequest.model()).thenReturn(Optional.empty());
Claude claude = new Claude(DEFAULT_MODEL, anthropicClient);
UnsupportedOperationException ex = null;
try {
claude.generateContent(llmRequest, false).toList().blockingGet();
} catch (UnsupportedOperationException e) {
ex = e;
}
assertNotNull((Object) ex);
// Ensure anthropic messages().create(...) was never called due to early failure
verify(anthropicClient.messages(), times(0)).create(any(MessageCreateParams.class));
}
}
Loading