Restructure Spring AI model module

- Pull chat, image, moderation, tool, audio specific api code into spring-ai-model module
  - Move the corresponding tests

Signed-off-by: Ilayaperumal Gopinathan <ilayaperumal.gopinathan@broadcom.com>
This commit is contained in:
Ilayaperumal Gopinathan
2025-03-28 13:20:41 +00:00
committed by Soby Chacko
parent 7f3852ee4d
commit 53af6fd5ac
197 changed files with 187 additions and 2500 deletions

View File

@@ -73,19 +73,6 @@
<version>${jsonschema.version}</version>
</dependency>
<!-- production dependencies -->
<dependency>
<groupId>org.antlr</groupId>
<artifactId>ST4</artifactId>
<version>${ST4.version}</version>
</dependency>
<dependency>
<groupId>org.antlr</groupId>
<artifactId>antlr4-runtime</artifactId>
<version>${antlr.version}</version>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>

View File

@@ -1,28 +0,0 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* 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
*
* https://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.
*/
/**
* Provides a set of interfaces and classes for a generic API designed to interact with
* various AI models. This package includes interfaces for handling AI model calls,
* requests, responses, results, and associated metadata. It is designed to offer a
* flexible and adaptable framework for interacting with different types of AI models,
* abstracting the complexities involved in model invocation and result processing. The
* use of generics enhances the API's capability to work with a wide range of models,
* ensuring a broad applicability across diverse AI scenarios.
*
*/
package org.springframework.ai.model;

View File

@@ -1,42 +0,0 @@
package org.springframework.ai.tool;
import org.junit.jupiter.api.Test;
import org.springframework.ai.tool.definition.ToolDefinition;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link ToolCallback}.
*
* @author Thomas Vitale
*/
class ToolCallbackTests {
@Test
void shouldOnlyImplementRequiredMethods() {
var testToolCallback = new TestToolCallback("test");
assertThat(testToolCallback.getToolDefinition()).isNotNull();
assertThat(testToolCallback.getToolMetadata()).isNotNull();
}
static class TestToolCallback implements ToolCallback {
private final ToolDefinition toolDefinition;
public TestToolCallback(String name) {
this.toolDefinition = ToolDefinition.builder().name(name).description(name).inputSchema("{}").build();
}
@Override
public ToolDefinition getToolDefinition() {
return toolDefinition;
}
@Override
public String call(String toolInput) {
return "";
}
}
}

View File

@@ -1,65 +0,0 @@
package org.springframework.ai.tool.definition;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Unit tests for {@link DefaultToolDefinition}.
*
* @author Thomas Vitale
*/
class DefaultToolDefinitionTests {
@Test
void shouldCreateDefaultToolDefinition() {
var toolDefinition = new DefaultToolDefinition("name", "description", "{}");
assertThat(toolDefinition.name()).isEqualTo("name");
assertThat(toolDefinition.description()).isEqualTo("description");
assertThat(toolDefinition.inputSchema()).isEqualTo("{}");
}
@Test
void shouldThrowExceptionWhenNameIsNull() {
assertThatThrownBy(() -> new DefaultToolDefinition(null, "description", "{}"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("name cannot be null or empty");
}
@Test
void shouldThrowExceptionWhenNameIsEmpty() {
assertThatThrownBy(() -> new DefaultToolDefinition("", "description", "{}"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("name cannot be null or empty");
}
@Test
void shouldThrowExceptionWhenDescriptionIsNull() {
assertThatThrownBy(() -> new DefaultToolDefinition("name", null, "{}"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("description cannot be null or empty");
}
@Test
void shouldThrowExceptionWhenDescriptionIsEmpty() {
assertThatThrownBy(() -> new DefaultToolDefinition("name", "", "{}"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("description cannot be null or empty");
}
@Test
void shouldThrowExceptionWhenInputSchemaIsNull() {
assertThatThrownBy(() -> new DefaultToolDefinition("name", "description", null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("inputSchema cannot be null or empty");
}
@Test
void shouldThrowExceptionWhenInputSchemaIsEmpty() {
assertThatThrownBy(() -> new DefaultToolDefinition("name", "description", ""))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("inputSchema cannot be null or empty");
}
}

View File

@@ -1,54 +0,0 @@
package org.springframework.ai.tool.definition;
import org.junit.jupiter.api.Test;
import org.springframework.ai.tool.annotation.Tool;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link ToolDefinition}.
*
* @author Thomas Vitale
*/
class ToolDefinitionTests {
@Test
void shouldCreateDefaultToolDefinitionBuilder() {
var toolDefinition = ToolDefinition.builder().name("name").description("description").inputSchema("{}").build();
assertThat(toolDefinition.name()).isEqualTo("name");
assertThat(toolDefinition.description()).isEqualTo("description");
assertThat(toolDefinition.inputSchema()).isEqualTo("{}");
}
@Test
void shouldCreateToolDefinitionFromMethod() {
var toolDefinition = ToolDefinition.from(Tools.class.getDeclaredMethods()[0]);
assertThat(toolDefinition.name()).isEqualTo("mySuperTool");
assertThat(toolDefinition.description()).isEqualTo("Test description");
assertThat(toolDefinition.inputSchema()).isEqualToIgnoringWhitespace("""
{
"$schema" : "https://json-schema.org/draft/2020-12/schema",
"type" : "object",
"properties" : {
"input" : {
"type" : "string"
}
},
"required" : [ "input" ],
"additionalProperties" : false
}
""");
}
static class Tools {
@Tool(description = "Test description")
public List<String> mySuperTool(String input) {
return List.of(input);
}
}
}

View File

@@ -1,95 +0,0 @@
package org.springframework.ai.tool.execution;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link DefaultToolCallResultConverter}.
*
* @author Thomas Vitale
*/
class DefaultToolCallResultConverterTests {
private final DefaultToolCallResultConverter converter = new DefaultToolCallResultConverter();
@Test
void convertWithNullReturnTypeShouldReturn() {
String result = converter.convert(null, null);
assertThat(result).isEqualTo("null");
}
@Test
void convertVoidReturnTypeShouldReturnDone() {
String result = converter.convert(null, void.class);
assertThat(result).isEqualTo("Done");
}
@Test
void convertStringReturnTypeShouldReturnJson() {
String result = converter.convert("test", String.class);
assertThat(result).isEqualTo("\"test\"");
}
@Test
void convertNullReturnValueShouldReturnNullJson() {
String result = converter.convert(null, String.class);
assertThat(result).isEqualTo("null");
}
@Test
void convertObjectReturnTypeShouldReturnJson() {
TestObject testObject = new TestObject("test", 42);
String result = converter.convert(testObject, TestObject.class);
assertThat(result).containsIgnoringWhitespaces("""
"name": "test"
""").containsIgnoringWhitespaces("""
"value": 42
""");
}
@Test
void convertCollectionReturnTypeShouldReturnJson() {
List<String> testList = List.of("one", "two", "three");
String result = converter.convert(testList, List.class);
assertThat(result).isEqualTo("""
["one","two","three"]
""".trim());
}
@Test
void convertMapReturnTypeShouldReturnJson() {
Map<String, Integer> testMap = Map.of("one", 1, "two", 2);
String result = converter.convert(testMap, Map.class);
assertThat(result).containsIgnoringWhitespaces("""
"one": 1
""").containsIgnoringWhitespaces("""
"two": 2
""");
}
static class TestObject {
private final String name;
private final int value;
TestObject(String name, int value) {
this.name = name;
this.value = value;
}
public String getName() {
return name;
}
public int getValue() {
return value;
}
}
}

View File

@@ -1,65 +0,0 @@
/*
* Copyright 2023-2025 the original author or authors.
*
* 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
*
* https://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 org.springframework.ai.tool.execution;
import org.junit.jupiter.api.Test;
import org.springframework.ai.tool.definition.DefaultToolDefinition;
import org.springframework.ai.tool.definition.ToolDefinition;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Unit tests for {@link DefaultToolExecutionExceptionProcessor}.
*
* @author Thomas Vitale
*/
class DefaultToolExecutionExceptionProcessorTests {
@Test
void whenDefaultThenReturnMessage() {
ToolExecutionExceptionProcessor processor = DefaultToolExecutionExceptionProcessor.builder().build();
ToolExecutionException exception = new ToolExecutionException(generateTestDefinition(),
new RuntimeException("Test"));
assertThat(processor.process(exception)).isEqualTo("Test");
}
@Test
void whenNotAlwaysThrowThenReturnMessage() {
ToolExecutionExceptionProcessor processor = DefaultToolExecutionExceptionProcessor.builder()
.alwaysThrow(false)
.build();
ToolExecutionException exception = new ToolExecutionException(generateTestDefinition(),
new RuntimeException("Test"));
assertThat(processor.process(exception)).isEqualTo("Test");
}
@Test
void whenAlwaysThrowThenThrow() {
ToolExecutionExceptionProcessor processor = DefaultToolExecutionExceptionProcessor.builder()
.alwaysThrow(true)
.build();
ToolExecutionException exception = new ToolExecutionException(generateTestDefinition(),
new RuntimeException("Test"));
assertThatThrownBy(() -> processor.process(exception)).isInstanceOf(ToolExecutionException.class);
}
private ToolDefinition generateTestDefinition() {
return DefaultToolDefinition.builder().name("test").inputSchema("{}").build();
}
}

View File

@@ -1,36 +0,0 @@
package org.springframework.ai.tool.execution;
import org.junit.jupiter.api.Test;
import org.springframework.ai.tool.definition.ToolDefinition;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Unit tests for {@link ToolExecutionException}.
*
* @author Thomas Vitale
*/
class ToolExecutionExceptionTests {
@Test
void constructorShouldSetCauseAndMessage() {
String errorMessage = "Test error message";
RuntimeException cause = new RuntimeException(errorMessage);
ToolExecutionException exception = new ToolExecutionException(mock(ToolDefinition.class), cause);
assertThat(exception.getCause()).isEqualTo(cause);
assertThat(exception.getMessage()).isEqualTo(errorMessage);
}
@Test
void getToolDefinitionShouldReturnToolDefinition() {
RuntimeException cause = new RuntimeException("Test error");
ToolDefinition toolDefinition = mock(ToolDefinition.class);
ToolExecutionException exception = new ToolExecutionException(toolDefinition, cause);
assertThat(exception.getToolDefinition()).isEqualTo(toolDefinition);
}
}

View File

@@ -1,268 +0,0 @@
/*
* Copyright 2023-2025 the original author or authors.
*
* 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
*
* https://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 org.springframework.ai.tool.function;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.definition.ToolDefinition;
import org.springframework.ai.tool.execution.ToolCallResultConverter;
import org.springframework.ai.tool.metadata.ToolMetadata;
import org.springframework.ai.util.json.schema.JsonSchemaGenerator;
import org.springframework.core.ParameterizedTypeReference;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Unit tests for {@link FunctionToolCallback}.
*
* @author Thomas Vitale
*/
class FunctionToolCallbackTests {
@Test
void constructorShouldValidateRequiredParameters() {
ToolDefinition toolDefinition = mock(ToolDefinition.class);
ToolMetadata toolMetadata = mock(ToolMetadata.class);
BiFunction<String, ToolContext, String> toolFunction = (input, context) -> input;
assertThatThrownBy(() -> new FunctionToolCallback<>(null, toolMetadata, String.class, toolFunction, null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("toolDefinition cannot be null");
assertThatThrownBy(() -> new FunctionToolCallback<>(toolDefinition, toolMetadata, null, toolFunction, null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("toolInputType cannot be null");
assertThatThrownBy(() -> new FunctionToolCallback<>(toolDefinition, toolMetadata, String.class, null, null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("toolFunction cannot be null");
}
@Test
void callShouldExecuteToolFunctionAndConvertResult() {
ToolDefinition toolDefinition = mock(ToolDefinition.class);
when(toolDefinition.name()).thenReturn("test-tool");
BiFunction<TestRequest, ToolContext, TestResponse> toolFunction = (input,
context) -> new TestResponse(input.input());
ToolCallback callback = FunctionToolCallback.builder("test-tool", toolFunction)
.inputType(TestRequest.class)
.build();
String result = callback.call("""
{
"input": "test input"
}
""", mock(ToolContext.class));
assertThat(result).isEqualToIgnoringWhitespace("""
{
"output": "test input"
}
""");
}
@Test
void callShouldValidateInput() {
ToolCallback callback = FunctionToolCallback.builder("test-tool", (input, context) -> input)
.inputType(String.class)
.build();
assertThatThrownBy(() -> callback.call("")).isInstanceOf(IllegalArgumentException.class)
.hasMessage("toolInput cannot be null or empty");
assertThatThrownBy(() -> callback.call(null)).isInstanceOf(IllegalArgumentException.class)
.hasMessage("toolInput cannot be null or empty");
}
@Test
void callWithoutContextShouldWorkCorrectly() {
BiFunction<TestRequest, ToolContext, TestResponse> toolFunction = (input,
context) -> new TestResponse(input.input());
ToolCallback callback = FunctionToolCallback.builder("test-tool", toolFunction)
.inputType(TestRequest.class)
.build();
String result = callback.call("""
{
"input": "test input"
}
""");
assertThat(result).isEqualToIgnoringWhitespace("""
{
"output": "test input"
}
""");
}
// Builder
@Test
void builderShouldCreateInstanceWithAllProperties() {
ToolMetadata toolMetadata = mock(ToolMetadata.class);
BiFunction<String, ToolContext, String> toolFunction = (input, context) -> input;
ToolCallResultConverter resultConverter = mock(ToolCallResultConverter.class);
ToolCallback callback = FunctionToolCallback.builder("testTool", toolFunction)
.description("A test tool")
.inputSchema(JsonSchemaGenerator.generateForType(String.class))
.inputType(String.class)
.toolMetadata(toolMetadata)
.toolCallResultConverter(resultConverter)
.build();
assertThat(callback.getToolDefinition().name()).isEqualTo("testTool");
assertThat(callback.getToolDefinition().description()).isEqualTo("A test tool");
assertThat(callback.getToolMetadata()).isEqualTo(toolMetadata);
}
@Test
void builderShouldCreateInstanceWithCustomSchema() {
ToolMetadata toolMetadata = mock(ToolMetadata.class);
BiFunction<String, ToolContext, String> toolFunction = (input, context) -> input;
ToolCallResultConverter resultConverter = mock(ToolCallResultConverter.class);
ToolCallback callback = FunctionToolCallback.builder("testTool", toolFunction)
.description("A test tool")
// Special schema generation required by Vertex AI.
.inputSchema(JsonSchemaGenerator.generateForType(String.class,
JsonSchemaGenerator.SchemaOption.UPPER_CASE_TYPE_VALUES))
.inputType(String.class)
.toolMetadata(toolMetadata)
.toolCallResultConverter(resultConverter)
.build();
assertThat(callback.getToolDefinition().name()).isEqualTo("testTool");
assertThat(callback.getToolDefinition().description()).isEqualTo("A test tool");
assertThat(callback.getToolMetadata()).isEqualTo(toolMetadata);
}
@Test
void whenBuilderWithRequiredPropertiesThenReturn() {
var builder = FunctionToolCallback.builder("test-tool", (input, context) -> input);
assertThat(builder).isNotNull();
}
@Test
void whenToolNameIsNullThenThrow() {
assertThatThrownBy(() -> FunctionToolCallback.builder(null, (input, context) -> input))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("name cannot be null or empty");
}
@Test
void whenToolNameIsEmptyThenThrow() {
assertThatThrownBy(() -> FunctionToolCallback.builder("", (input, context) -> input))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("name cannot be null or empty");
}
@Test
void whenBuildingFromBiFunctionThenReturn() {
var builder = FunctionToolCallback.builder("test-tool", (input, context) -> input);
assertThat(builder).isNotNull();
}
@Test
void whenBuildingFromNullBiFunctionThenReturn() {
assertThatThrownBy(() -> FunctionToolCallback.builder("test-tool", (BiFunction<?, ToolContext, ?>) null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("toolFunction cannot be null");
}
@Test
void whenBuildingFromFunctionThenReturn() {
var builder = FunctionToolCallback.builder("test-tool", (input) -> input);
assertThat(builder).isNotNull();
}
@Test
void whenBuildingFromNullFunctionThenReturn() {
assertThatThrownBy(() -> FunctionToolCallback.builder("test-tool", (Function<?, ?>) null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("function cannot be null");
}
@Test
void whenBuildingFromSupplierThenReturn() {
var builder = FunctionToolCallback.builder("test-tool", () -> "Hello");
assertThat(builder).isNotNull();
}
@Test
void whenBuildingFromNullSupplierThenReturn() {
assertThatThrownBy(() -> FunctionToolCallback.builder("test-tool", (Supplier<?>) null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("supplier cannot be null");
}
@Test
void whenBuildingFromConsumerThenReturn() {
var builder = FunctionToolCallback.builder("test-tool", (input) -> null);
assertThat(builder).isNotNull();
}
@Test
void whenBuildingFromNullConsumerThenReturn() {
assertThatThrownBy(() -> FunctionToolCallback.builder("test-tool", (Consumer<?>) null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("consumer cannot be null");
}
@Test
void whenInputTypeIsNullThenThrow() {
assertThatThrownBy(() -> FunctionToolCallback.builder("test-tool", (input, context) -> input).build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("inputType cannot be null");
}
@Test
void whenToolDescriptionIsNullThenComputeFromName() {
ToolCallback callback = FunctionToolCallback.builder("mySuperTestTool", (input, context) -> input)
.inputType(String.class)
.build();
assertThat(callback.getToolDefinition().description()).isEqualTo("my super test tool");
}
@Test
void whenInputTypeIsGenericThenReturn() {
ToolCallback callback = FunctionToolCallback.builder("mySuperTestTool", (input, context) -> input)
.inputType(new ParameterizedTypeReference<List<String>>() {
})
.build();
assertThat(callback).isNotNull();
}
public record TestRequest(String input) {
}
public record TestResponse(String output) {
}
}

View File

@@ -1,26 +0,0 @@
package org.springframework.ai.tool.metadata;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link DefaultToolMetadata}.
*
* @author Thomas Vitale
*/
class DefaultToolMetadataTests {
@Test
void shouldCreateDefaultToolMetadataWithDefaultValues() {
var toolMetadata = DefaultToolMetadata.builder().build();
assertThat(toolMetadata.returnDirect()).isFalse();
}
@Test
void shouldCreateDefaultToolMetadataWithGivenValues() {
var toolMetadata = DefaultToolMetadata.builder().returnDirect(true).build();
assertThat(toolMetadata.returnDirect()).isTrue();
}
}

View File

@@ -1,38 +0,0 @@
package org.springframework.ai.tool.metadata;
import org.junit.jupiter.api.Test;
import org.springframework.ai.tool.annotation.Tool;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link ToolMetadata}.
*
* @author Thomas Vitale
*/
class ToolMetadataTests {
@Test
void shouldCreateDefaultToolMetadataBuilder() {
var toolMetadata = ToolMetadata.builder().build();
assertThat(toolMetadata.returnDirect()).isFalse();
}
@Test
void shouldCreateToolMetadataFromMethod() {
var toolMetadata = ToolMetadata.from(Tools.class.getDeclaredMethods()[0]);
assertThat(toolMetadata.returnDirect()).isTrue();
}
static class Tools {
@Tool(description = "Test description", returnDirect = true)
public List<String> mySuperTool(String input) {
return List.of(input);
}
}
}

View File

@@ -1,219 +0,0 @@
/*
* Copyright 2025-2025 the original author or authors.
*
* 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
*
* https://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 org.springframework.ai.tool.method;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.AopUtils;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
/**
* Tests for {@link MethodToolCallbackProvider} with AOP proxies.
*
* @author Christian Tzolov
*/
@ExtendWith(MockitoExtension.class)
class MethodToolCallbackProviderAopTests {
/**
* Test annotation to simulate a Spring AOP aspect
*/
@java.lang.annotation.Target({ java.lang.annotation.ElementType.METHOD })
@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@java.lang.annotation.Documented
public @interface LogExecution {
}
/**
* Sample bean with methods annotated with both @Tool and @LogExecution
*/
@Component
static class ToolsWithAopAnnotations {
@Tool(description = "Method with AOP annotation")
@LogExecution
public String methodWithAopAnnotation(String input) {
return "Processed: " + input;
}
@Tool(description = "Another method with AOP annotation")
@LogExecution
public List<String> anotherMethodWithAopAnnotation(String input) {
return List.of("Item: " + input);
}
@Tool(description = "Method without AOP annotation")
public String methodWithoutAopAnnotation(String input) {
return "Regular: " + input;
}
}
@Test
void shouldHandleAopProxiedToolObject() {
// Create the original tool object
ToolsWithAopAnnotations originalToolObject = new ToolsWithAopAnnotations();
// Create a proxy for the tool object with an aspect for @LogExecution annotation
ProxyFactory proxyFactory = new ProxyFactory(originalToolObject);
AnnotationMatchingPointcut pointcut = new AnnotationMatchingPointcut(null, LogExecution.class);
// Create a method interceptor for logging
MethodInterceptor loggingInterceptor = new MethodInterceptor() {
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
// Simple logging advice
System.out.println("Before executing: " + methodInvocation.getMethod().getName());
Object result = methodInvocation.proceed();
System.out.println("After executing: " + methodInvocation.getMethod().getName());
return result;
}
};
proxyFactory.addAdvisor(new DefaultPointcutAdvisor(pointcut, loggingInterceptor));
Object proxiedToolObject = proxyFactory.getProxy();
// Verify that the object is indeed a proxy
assertThat(AopUtils.isAopProxy(proxiedToolObject)).isTrue();
assertThat(AopUtils.getTargetClass(proxiedToolObject)).isEqualTo(ToolsWithAopAnnotations.class);
// Create the provider with the proxied object
MethodToolCallbackProvider provider = MethodToolCallbackProvider.builder()
.toolObjects(proxiedToolObject)
.build();
// Get the tool callbacks
ToolCallback[] callbacks = provider.getToolCallbacks();
// Verify that all methods with @Tool annotation are found, including those with
// @LogExecution
assertThat(callbacks).hasSize(3);
// Verify that the tool names match the expected method names
assertThat(Stream.of(callbacks).map(ToolCallback::getName)).containsExactlyInAnyOrder("methodWithAopAnnotation",
"anotherMethodWithAopAnnotation", "methodWithoutAopAnnotation");
}
/**
* This test specifically validates the AOP proxy handling logic in
* MethodToolCallbackProvider. It uses Mockito to verify that AopUtils.isAopProxy and
* AopUtils.getTargetClass are called correctly when processing a proxied object.
*/
@Test
void shouldUseAopUtilsToHandleProxiedObjects() {
// Create the original tool object
ToolsWithAopAnnotations originalToolObject = new ToolsWithAopAnnotations();
// Create a proxy for the tool object
ProxyFactory proxyFactory = new ProxyFactory(originalToolObject);
AnnotationMatchingPointcut pointcut = new AnnotationMatchingPointcut(null, LogExecution.class);
MethodInterceptor loggingInterceptor = new MethodInterceptor() {
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
return methodInvocation.proceed();
}
};
proxyFactory.addAdvisor(new DefaultPointcutAdvisor(pointcut, loggingInterceptor));
Object proxiedToolObject = proxyFactory.getProxy();
// Use MockedStatic to verify AopUtils static methods are called
try (MockedStatic<AopUtils> mockedAopUtils = Mockito.mockStatic(AopUtils.class)) {
// Set up the mocked behavior
mockedAopUtils.when(() -> AopUtils.isAopProxy(any())).thenReturn(true);
mockedAopUtils.when(() -> AopUtils.getTargetClass(any())).thenReturn(ToolsWithAopAnnotations.class);
// Create the provider with the proxied object
MethodToolCallbackProvider provider = MethodToolCallbackProvider.builder()
.toolObjects(proxiedToolObject)
.build();
// Get the tool callbacks - this should trigger the AopUtils methods
provider.getToolCallbacks();
// Verify that AopUtils.isAopProxy was called with the proxied object
mockedAopUtils.verify(() -> AopUtils.isAopProxy(proxiedToolObject), times(1));
// Verify that AopUtils.getTargetClass was called with the proxied object
mockedAopUtils.verify(() -> AopUtils.getTargetClass(proxiedToolObject), times(1));
}
}
@Test
void shouldHandleMixOfProxiedAndNonProxiedToolObjects() {
// Create the original tool objects
ToolsWithAopAnnotations originalToolObject = new ToolsWithAopAnnotations();
// Create a proxy for one of the tool objects
ProxyFactory proxyFactory = new ProxyFactory(originalToolObject);
AnnotationMatchingPointcut pointcut = new AnnotationMatchingPointcut(null, LogExecution.class);
// Create a method interceptor for logging
MethodInterceptor loggingInterceptor = new MethodInterceptor() {
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
// Simple logging advice
System.out.println("Before executing: " + methodInvocation.getMethod().getName());
Object result = methodInvocation.proceed();
System.out.println("After executing: " + methodInvocation.getMethod().getName());
return result;
}
};
proxyFactory.addAdvisor(new DefaultPointcutAdvisor(pointcut, loggingInterceptor));
Object proxiedToolObject = proxyFactory.getProxy();
// Create a non-proxied tool object
MethodToolCallbackProviderTests.ToolsExtra nonProxiedToolObject = new MethodToolCallbackProviderTests.ToolsExtra();
// Create the provider with both proxied and non-proxied objects
MethodToolCallbackProvider provider = MethodToolCallbackProvider.builder()
.toolObjects(proxiedToolObject, nonProxiedToolObject)
.build();
// Get the tool callbacks
ToolCallback[] callbacks = provider.getToolCallbacks();
// Verify that all methods with @Tool annotation are found from both objects
assertThat(callbacks).hasSize(5); // 3 from proxied + 2 from non-proxied
// Verify that the tool names match the expected method names
assertThat(Stream.of(callbacks).map(ToolCallback::getName)).containsExactlyInAnyOrder("methodWithAopAnnotation",
"anotherMethodWithAopAnnotation", "methodWithoutAopAnnotation", "extraMethod1", "extraMethod2");
}
}

View File

@@ -1,271 +0,0 @@
/*
* Copyright 2025-2025 the original author or authors.
*
* 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
*
* https://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 org.springframework.ai.tool.method;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.annotation.Tool;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Unit tests for {@link MethodToolCallbackProvider}.
*
* @author Thomas Vitale
*/
class MethodToolCallbackProviderTests {
@Nested
class BuilderValidationTests {
@Test
void shouldRejectNullToolObjects() {
assertThatThrownBy(() -> MethodToolCallbackProvider.builder().toolObjects((Object[]) null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("toolObjects cannot be null");
}
@Test
void shouldRejectNullToolObjectElements() {
assertThatThrownBy(() -> MethodToolCallbackProvider.builder().toolObjects(new Tools(), null).build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("toolObjects cannot contain null elements");
}
@Test
void shouldAcceptEmptyToolObjects() {
var provider = MethodToolCallbackProvider.builder().toolObjects().build();
assertThat(provider.getToolCallbacks()).isEmpty();
}
}
@Test
void shouldProvideToolCallbacksFromObject() {
Tools tools = new Tools();
MethodToolCallbackProvider provider = MethodToolCallbackProvider.builder().toolObjects(tools).build();
ToolCallback[] callbacks = provider.getToolCallbacks();
assertThat(callbacks).hasSize(2);
var callback1 = Stream.of(callbacks).filter(c -> c.getToolDefinition().name().equals("testMethod")).findFirst();
assertThat(callback1).isPresent();
assertThat(callback1.get().getToolDefinition().name()).isEqualTo("testMethod");
assertThat(callback1.get().getToolDefinition().description()).isEqualTo("Test description");
var callback2 = Stream.of(callbacks)
.filter(c -> c.getToolDefinition().name().equals("testStaticMethod"))
.findFirst();
assertThat(callback2).isPresent();
assertThat(callback2.get().getToolDefinition().name()).isEqualTo("testStaticMethod");
assertThat(callback2.get().getToolDefinition().description()).isEqualTo("Test description");
}
@Test
void shouldProvideToolCallbacksFromMultipleObjects() {
Tools tools1 = new Tools();
ToolsExtra tools2 = new ToolsExtra();
MethodToolCallbackProvider provider = MethodToolCallbackProvider.builder().toolObjects(tools1, tools2).build();
ToolCallback[] callbacks = provider.getToolCallbacks();
assertThat(callbacks).hasSize(4); // 2 from Tools + 2 from ToolsExtra
assertThat(Stream.of(callbacks).map(ToolCallback::getName)).containsExactlyInAnyOrder("testMethod",
"testStaticMethod", "extraMethod1", "extraMethod2");
}
@Test
void shouldEnsureUniqueToolNames() {
ToolsWithDuplicates testComponent = new ToolsWithDuplicates();
MethodToolCallbackProvider provider = MethodToolCallbackProvider.builder().toolObjects(testComponent).build();
assertThatThrownBy(provider::getToolCallbacks).isInstanceOf(IllegalStateException.class)
.hasMessageContaining("Multiple tools with the same name (testMethod) found in sources: "
+ testComponent.getClass().getName());
}
@Test
void shouldHandleToolMethodsWithDifferentVisibility() {
ToolsWithVisibility tools = new ToolsWithVisibility();
MethodToolCallbackProvider provider = MethodToolCallbackProvider.builder().toolObjects(tools).build();
ToolCallback[] callbacks = provider.getToolCallbacks();
assertThat(callbacks).hasSize(3);
assertThat(Stream.of(callbacks).map(ToolCallback::getName)).containsExactlyInAnyOrder("publicMethod",
"protectedMethod", "privateMethod");
}
@Test
void shouldHandleToolMethodsWithDifferentParameters() {
ToolsWithParameters tools = new ToolsWithParameters();
MethodToolCallbackProvider provider = MethodToolCallbackProvider.builder().toolObjects(tools).build();
ToolCallback[] callbacks = provider.getToolCallbacks();
assertThat(callbacks).hasSize(3);
assertThat(Stream.of(callbacks).map(ToolCallback::getName)).containsExactlyInAnyOrder("noParams", "oneParam",
"multipleParams");
}
@Test
void shouldHandleToolMethodsWithDifferentReturnTypes() {
ToolsWithReturnTypes tools = new ToolsWithReturnTypes();
MethodToolCallbackProvider provider = MethodToolCallbackProvider.builder().toolObjects(tools).build();
ToolCallback[] callbacks = provider.getToolCallbacks();
assertThat(callbacks).hasSize(4);
assertThat(Stream.of(callbacks).map(ToolCallback::getName)).containsExactlyInAnyOrder("voidMethod",
"primitiveMethod", "objectMethod", "collectionMethod");
}
static class Tools {
@Tool(description = "Test description")
static List<String> testStaticMethod(String input) {
return List.of(input);
}
@Tool(description = "Test description")
List<String> testMethod(String input) {
return List.of(input);
}
@Tool(description = "Test description")
Function<String, Integer> testFunction(String input) {
// This method should be ignored as it's a functional type
return String::length;
}
@Tool(description = "Test description")
Consumer<String> testConsumer(String input) {
// This method should be ignored as it's a functional type
return System.out::println;
}
@Tool(description = "Test description")
Supplier<String> testSupplier() {
// This method should be ignored as it's a functional type
return () -> "test";
}
void nonToolMethod() {
// This method should be ignored as it doesn't have @Tool annotation
}
}
static class ToolsExtra {
@Tool(description = "Extra method 1")
String extraMethod1() {
return "extra1";
}
@Tool(description = "Extra method 2")
String extraMethod2() {
return "extra2";
}
}
static class ToolsWithDuplicates {
@Tool(name = "testMethod", description = "Test description")
List<String> testMethod1(String input) {
return List.of(input);
}
@Tool(name = "testMethod", description = "Test description")
List<String> testMethod2(String input) {
return List.of(input);
}
}
static class ToolsWithVisibility {
@Tool(description = "Public method")
public String publicMethod() {
return "public";
}
@Tool(description = "Protected method")
protected String protectedMethod() {
return "protected";
}
@Tool(description = "Private method")
private String privateMethod() {
return "private";
}
}
static class ToolsWithParameters {
@Tool(description = "No parameters")
String noParams() {
return "no params";
}
@Tool(description = "One parameter")
String oneParam(String param) {
return param;
}
@Tool(description = "Multiple parameters")
String multipleParams(String param1, int param2, boolean param3) {
return param1 + param2 + param3;
}
}
static class ToolsWithReturnTypes {
@Tool(description = "Void method")
void voidMethod() {
}
@Tool(description = "Primitive method")
int primitiveMethod() {
return 42;
}
@Tool(description = "Object method")
String objectMethod() {
return "object";
}
@Tool(description = "Collection method")
List<String> collectionMethod() {
return List.of("collection");
}
}
}

View File

@@ -1,323 +0,0 @@
/*
* Copyright 2025-2025 the original author or authors.
*
* 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
*
* https://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 org.springframework.ai.tool.method;
import com.fasterxml.jackson.core.type.TypeReference;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.definition.ToolDefinition;
import org.springframework.ai.tool.execution.ToolExecutionException;
import org.springframework.ai.tool.metadata.ToolMetadata;
import org.springframework.ai.util.json.JsonParser;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Unit tests for {@link MethodToolCallback}.
*
* @author Thomas Vitale
*/
class MethodToolCallbackTests {
@ParameterizedTest
@ValueSource(strings = { "publicStaticMethod", "privateStaticMethod", "packageStaticMethod", "publicMethod",
"privateMethod", "packageMethod" })
void shouldCallToolFromPublicClass(String methodName) {
validateAssertions(methodName, new PublicTools());
}
@ParameterizedTest
@ValueSource(strings = { "publicStaticMethod", "privateStaticMethod", "packageStaticMethod", "publicMethod",
"privateMethod", "packageMethod" })
void shouldCallToolFromPrivateClass(String methodName) {
validateAssertions(methodName, new PrivateTools());
}
@ParameterizedTest
@ValueSource(strings = { "publicStaticMethod", "privateStaticMethod", "packageStaticMethod", "publicMethod",
"privateMethod", "packageMethod" })
void shouldCallToolFromPackageClass(String methodName) {
validateAssertions(methodName, new PackageTools());
}
@Test
void shouldHandleToolContextWhenSupported() {
Method toolMethod = getMethod("methodWithToolContext", ToolContextTools.class);
MethodToolCallback callback = MethodToolCallback.builder()
.toolDefinition(ToolDefinition.from(toolMethod))
.toolMetadata(ToolMetadata.from(toolMethod))
.toolMethod(toolMethod)
.toolObject(new ToolContextTools())
.build();
ToolContext toolContext = new ToolContext(Map.of("key", "value"));
String result = callback.call("""
{
"input": "test"
}
""", toolContext);
assertThat(result).contains("value");
}
@Test
void shouldThrowExceptionWhenToolContextArgumentIsMissing() {
Method toolMethod = getMethod("methodWithToolContext", ToolContextTools.class);
MethodToolCallback callback = MethodToolCallback.builder()
.toolDefinition(ToolDefinition.from(toolMethod))
.toolMetadata(ToolMetadata.from(toolMethod))
.toolMethod(toolMethod)
.toolObject(new PublicTools())
.build();
assertThatThrownBy(() -> callback.call("""
{
"input": "test"
}
""")).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("ToolContext is required by the method as an argument");
}
@Test
void shouldHandleComplexArguments() {
Method toolMethod = getMethod("complexArgumentMethod", ComplexTools.class);
MethodToolCallback callback = MethodToolCallback.builder()
.toolDefinition(ToolDefinition.from(toolMethod))
.toolMetadata(ToolMetadata.from(toolMethod))
.toolMethod(toolMethod)
.toolObject(new ComplexTools())
.build();
String result = callback.call("""
{
"stringArg": "test",
"intArg": 42,
"listArg": ["a", "b", "c"],
"optionalArg": null
}
""");
assertThat(JsonParser.fromJson(result, new TypeReference<Map<String, Object>>() {
})).containsEntry("stringValue", "test").containsEntry("intValue", 42).containsEntry("listSize", 3);
}
@Test
void shouldHandleCustomResultConverter() {
Method toolMethod = getMethod("publicMethod", PublicTools.class);
MethodToolCallback callback = MethodToolCallback.builder()
.toolDefinition(ToolDefinition.from(toolMethod))
.toolMetadata(ToolMetadata.from(toolMethod))
.toolMethod(toolMethod)
.toolObject(new PublicTools())
.toolCallResultConverter((result, type) -> "Converted: " + result)
.build();
String result = callback.call("""
{
"input": "test"
}
""");
assertThat(result).startsWith("Converted:");
}
@Test
void shouldThrowExceptionWhenToolExecutionFails() {
Method toolMethod = getMethod("errorMethod", ErrorTools.class);
MethodToolCallback callback = MethodToolCallback.builder()
.toolDefinition(ToolDefinition.from(toolMethod))
.toolMetadata(ToolMetadata.from(toolMethod))
.toolMethod(toolMethod)
.toolObject(new ErrorTools())
.build();
assertThatThrownBy(() -> callback.call("""
{
"input": "test"
}
""")).isInstanceOf(ToolExecutionException.class).hasMessageContaining("Test error");
}
private static void validateAssertions(String methodName, Object toolObject) {
Method toolMethod = getMethod(methodName, toolObject.getClass());
assertThat(toolMethod).isNotNull();
MethodToolCallback callback = MethodToolCallback.builder()
.toolDefinition(ToolDefinition.from(toolMethod))
.toolMetadata(ToolMetadata.from(toolMethod))
.toolMethod(toolMethod)
.toolObject(toolObject)
.build();
String result = callback.call("""
{
"input": "Wingardium Leviosa"
}
""");
assertThat(JsonParser.fromJson(result, new TypeReference<List<String>>() {
})).contains("Wingardium Leviosa");
}
private static Method getMethod(String name, Class<?> toolsClass) {
return Arrays.stream(ReflectionUtils.getDeclaredMethods(toolsClass))
.filter(m -> m.getName().equals(name))
.findFirst()
.orElseThrow();
}
static public class PublicTools {
@Tool(description = "Test description")
public static List<String> publicStaticMethod(String input) {
return List.of(input);
}
@Tool(description = "Test description")
private static List<String> privateStaticMethod(String input) {
return List.of(input);
}
@Tool(description = "Test description")
static List<String> packageStaticMethod(String input) {
return List.of(input);
}
@Tool(description = "Test description")
public List<String> publicMethod(String input) {
return List.of(input);
}
@Tool(description = "Test description")
private List<String> privateMethod(String input) {
return List.of(input);
}
@Tool(description = "Test description")
List<String> packageMethod(String input) {
return List.of(input);
}
}
static private class PrivateTools {
@Tool(description = "Test description")
public static List<String> publicStaticMethod(String input) {
return List.of(input);
}
@Tool(description = "Test description")
private static List<String> privateStaticMethod(String input) {
return List.of(input);
}
@Tool(description = "Test description")
static List<String> packageStaticMethod(String input) {
return List.of(input);
}
@Tool(description = "Test description")
public List<String> publicMethod(String input) {
return List.of(input);
}
@Tool(description = "Test description")
private List<String> privateMethod(String input) {
return List.of(input);
}
@Tool(description = "Test description")
List<String> packageMethod(String input) {
return List.of(input);
}
}
static class PackageTools {
@Tool(description = "Test description")
public static List<String> publicStaticMethod(String input) {
return List.of(input);
}
@Tool(description = "Test description")
private static List<String> privateStaticMethod(String input) {
return List.of(input);
}
@Tool(description = "Test description")
static List<String> packageStaticMethod(String input) {
return List.of(input);
}
@Tool(description = "Test description")
public List<String> publicMethod(String input) {
return List.of(input);
}
@Tool(description = "Test description")
private List<String> privateMethod(String input) {
return List.of(input);
}
@Tool(description = "Test description")
List<String> packageMethod(String input) {
return List.of(input);
}
}
static class ToolContextTools {
@Tool(description = "Test description")
public String methodWithToolContext(String input, ToolContext toolContext) {
return input + ": " + toolContext.getContext().get("key");
}
}
static class ComplexTools {
@Tool(description = "Test description")
public Map<String, Object> complexArgumentMethod(String stringArg, int intArg, List<String> listArg,
String optionalArg) {
return Map.of("stringValue", stringArg, "intValue", intArg, "listSize", listArg.size(), "optionalProvided",
optionalArg != null);
}
}
static class ErrorTools {
@Tool(description = "Test description")
public String errorMethod(String input) {
throw new IllegalArgumentException("Test error");
}
}
}

View File

@@ -1,64 +0,0 @@
/*
* Copyright 2023-2025 the original author or authors.
*
* 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
*
* https://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 org.springframework.ai.tool.resolution;
import org.junit.jupiter.api.Test;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.definition.ToolDefinition;
import java.util.ArrayList;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Unit tests for {@link DelegatingToolCallbackResolver}.
*
* @author Thomas Vitale
*/
class DelegatingToolCallbackResolverTests {
@Test
void whenToolCallbackResolversAreNullThenThrowException() {
assertThatThrownBy(() -> new DelegatingToolCallbackResolver(null)).isInstanceOf(IllegalArgumentException.class);
}
@Test
void whenToolCallbackResolversContainNullElementsThenThrowException() {
var toolCallbackResolvers = new ArrayList<ToolCallbackResolver>();
toolCallbackResolvers.add(null);
assertThatThrownBy(() -> new DelegatingToolCallbackResolver(toolCallbackResolvers))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
void whenToolCallbacksAreProvidedThenResolveToolCallback() {
ToolCallback toolCallback = mock(ToolCallback.class);
when(toolCallback.getToolDefinition())
.thenReturn(ToolDefinition.builder().name("myTool").inputSchema("{}").build());
StaticToolCallbackResolver staticToolCallbackResolver = new StaticToolCallbackResolver(List.of(toolCallback));
DelegatingToolCallbackResolver delegatingToolCallbackResolver = new DelegatingToolCallbackResolver(
List.of(staticToolCallbackResolver));
assertThat(delegatingToolCallbackResolver.resolve("myTool")).isEqualTo(toolCallback);
}
}

View File

@@ -1,175 +0,0 @@
/*
* Copyright 2023-2025 the original author or authors.
*
* 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
*
* https://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 org.springframework.ai.tool.resolution;
import org.junit.jupiter.api.Test;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.util.json.schema.SchemaType;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Description;
import org.springframework.context.support.GenericApplicationContext;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Unit tests for {@link SpringBeanToolCallbackResolver}.
*
* @author Thomas Vitale
*/
class SpringBeanToolCallbackResolverTests {
@Test
void whenApplicationContextIsNullThenThrow() {
assertThatThrownBy(() -> new SpringBeanToolCallbackResolver(null, null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("applicationContext cannot be null");
assertThatThrownBy(() -> SpringBeanToolCallbackResolver.builder().build())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("applicationContext cannot be null");
}
@Test
void whenSchemaTypeIsNullThenUseDefault() {
SpringBeanToolCallbackResolver resolver = new SpringBeanToolCallbackResolver(new GenericApplicationContext(),
null);
assertThat(resolver.getSchemaType()).isEqualTo(SchemaType.JSON_SCHEMA);
SpringBeanToolCallbackResolver resolver2 = SpringBeanToolCallbackResolver.builder()
.applicationContext(new GenericApplicationContext())
.build();
assertThat(resolver2.getSchemaType()).isEqualTo(SchemaType.JSON_SCHEMA);
}
@Test
void whenSchemaTypeIsNotNullThenUseIt() {
SchemaType schemaType = SchemaType.OPEN_API_SCHEMA;
SpringBeanToolCallbackResolver resolver = new SpringBeanToolCallbackResolver(new GenericApplicationContext(),
schemaType);
assertThat(resolver.getSchemaType()).isEqualTo(schemaType);
SpringBeanToolCallbackResolver resolver2 = SpringBeanToolCallbackResolver.builder()
.applicationContext(new GenericApplicationContext())
.schemaType(schemaType)
.build();
assertThat(resolver2.getSchemaType()).isEqualTo(schemaType);
}
@Test
void whenRequiredArgumentsAreProvidedThenCreateInstance() {
GenericApplicationContext applicationContext = new GenericApplicationContext();
SchemaType schemaType = SchemaType.OPEN_API_SCHEMA;
SpringBeanToolCallbackResolver resolver = new SpringBeanToolCallbackResolver(applicationContext, schemaType);
assertThat(resolver).isNotNull();
SpringBeanToolCallbackResolver resolver2 = SpringBeanToolCallbackResolver.builder()
.applicationContext(applicationContext)
.schemaType(schemaType)
.build();
assertThat(resolver2).isNotNull();
}
@Test
void whenToolCallbackWithVoidConsumerIsResolvedThenReturnIt() {
GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext(Functions.class);
SpringBeanToolCallbackResolver resolver = new SpringBeanToolCallbackResolver(applicationContext,
SchemaType.JSON_SCHEMA);
ToolCallback resolvedToolCallback = resolver.resolve(Functions.WELCOME_TOOL_NAME);
assertThat(resolvedToolCallback).isNotNull();
assertThat(resolvedToolCallback.getToolDefinition().name()).isEqualTo(Functions.WELCOME_TOOL_NAME);
assertThat(resolvedToolCallback.getToolDefinition().description())
.isEqualTo(Functions.WELCOME_TOOL_DESCRIPTION);
}
@Test
void whenToolCallbackWithConsumerIsResolvedThenReturnIt() {
GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext(Functions.class);
SpringBeanToolCallbackResolver resolver = new SpringBeanToolCallbackResolver(applicationContext,
SchemaType.JSON_SCHEMA);
ToolCallback resolvedToolCallback = resolver.resolve(Functions.WELCOME_USER_TOOL_NAME);
assertThat(resolvedToolCallback).isNotNull();
assertThat(resolvedToolCallback.getToolDefinition().name()).isEqualTo(Functions.WELCOME_USER_TOOL_NAME);
assertThat(resolvedToolCallback.getToolDefinition().description())
.isEqualTo(Functions.WELCOME_USER_TOOL_DESCRIPTION);
}
@Test
void whenToolCallbackWithFunctionIsResolvedThenReturnIt() {
GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext(Functions.class);
SpringBeanToolCallbackResolver resolver = new SpringBeanToolCallbackResolver(applicationContext,
SchemaType.JSON_SCHEMA);
ToolCallback resolvedToolCallback = resolver.resolve(Functions.BOOKS_BY_AUTHOR_TOOL_NAME);
assertThat(resolvedToolCallback).isNotNull();
assertThat(resolvedToolCallback.getToolDefinition().name()).isEqualTo(Functions.BOOKS_BY_AUTHOR_TOOL_NAME);
assertThat(resolvedToolCallback.getToolDefinition().description())
.isEqualTo(Functions.BOOKS_BY_AUTHOR_TOOL_DESCRIPTION);
}
@Configuration(proxyBeanMethods = false)
static class Functions {
public static final String BOOKS_BY_AUTHOR_TOOL_NAME = "booksByAuthor";
public static final String BOOKS_BY_AUTHOR_TOOL_DESCRIPTION = "Get the list of books written by the given author available in the library";
public static final String WELCOME_TOOL_NAME = "welcome";
public static final String WELCOME_TOOL_DESCRIPTION = "Welcome users to the library";
public static final String WELCOME_USER_TOOL_NAME = "welcomeUser";
public static final String WELCOME_USER_TOOL_DESCRIPTION = "Welcome a specific user to the library";
@Bean(WELCOME_TOOL_NAME)
@Description(WELCOME_TOOL_DESCRIPTION)
Consumer<Void> welcome() {
return (input) -> {
};
}
@Bean(WELCOME_USER_TOOL_NAME)
@Description(WELCOME_USER_TOOL_DESCRIPTION)
Consumer<User> welcomeUser() {
return user -> {
};
}
@Bean(BOOKS_BY_AUTHOR_TOOL_NAME)
@Description(BOOKS_BY_AUTHOR_TOOL_DESCRIPTION)
Function<Author, List<Book>> booksByAuthor() {
return author -> List.of(new Book("Book 1", author.name()), new Book("Book 2", author.name()));
}
public record User(String name) {
}
public record Author(String name) {
}
public record Book(String title, String author) {
}
}
}

View File

@@ -1,34 +0,0 @@
/*
* Copyright 2023-2025 the original author or authors.
*
* 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
*
* https://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 org.springframework.ai.tool.resolution;
import java.util.function.Function;
import org.springframework.ai.tool.resolution.TypeResolverHelperIT.WeatherRequest;
import org.springframework.ai.tool.resolution.TypeResolverHelperIT.WeatherResponse;
/**
* @author Christian Tzolov
*/
public class StandaloneWeatherFunction implements Function<WeatherRequest, WeatherResponse> {
@Override
public WeatherResponse apply(WeatherRequest weatherRequest) {
return new WeatherResponse(42.0f);
}
}

View File

@@ -1,67 +0,0 @@
/*
* Copyright 2023-2025 the original author or authors.
*
* 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
*
* https://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 org.springframework.ai.tool.resolution;
import org.junit.jupiter.api.Test;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.definition.ToolDefinition;
import java.util.ArrayList;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Unit tests for {@link StaticToolCallbackResolver}.
*
* @author Thomas Vitale
*/
class StaticToolCallbackResolverTests {
@Test
void whenToolCallbacksAreNullThenThrowException() {
assertThatThrownBy(() -> new StaticToolCallbackResolver(null)).isInstanceOf(IllegalArgumentException.class);
}
@Test
void whenToolCallbacksContainNullElementsThenThrowException() {
var toolCallbacks = new ArrayList<FunctionCallback>();
toolCallbacks.add(null);
assertThatThrownBy(() -> new StaticToolCallbackResolver(toolCallbacks))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
void whenToolCallbacksAreEmptyThenReturn() {
StaticToolCallbackResolver resolver = new StaticToolCallbackResolver(List.of());
assertThat(resolver).isNotNull();
}
@Test
void whenToolCallbacksAreProvidedThenResolveToolCallback() {
ToolCallback toolCallback = mock(ToolCallback.class);
when(toolCallback.getToolDefinition())
.thenReturn(ToolDefinition.builder().name("myTool").inputSchema("{}").build());
StaticToolCallbackResolver resolver = new StaticToolCallbackResolver(List.of(toolCallback));
assertThat(resolver.resolve("myTool")).isEqualTo(toolCallback);
}
}

View File

@@ -1,99 +0,0 @@
/*
* Copyright 2023-2025 the original author or authors.
*
* 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
*
* https://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 org.springframework.ai.tool.resolution;
import java.util.function.Consumer;
import java.util.function.Function;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.ResolvableType;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
public class TypeResolverHelperIT {
@Autowired
GenericApplicationContext applicationContext;
@ParameterizedTest(name = "{0} : {displayName} ")
@ValueSource(strings = { "weatherClassDefinition", "weatherFunctionDefinition", "standaloneWeatherFunction",
"scannedStandaloneWeatherFunction", "componentWeatherFunction", "weatherConsumer" })
void beanInputTypeResolutionWithResolvableType(String beanName) {
assertThat(this.applicationContext).isNotNull();
ResolvableType functionType = TypeResolverHelper.resolveBeanType(this.applicationContext, beanName);
Class<?> functionInputClass = TypeResolverHelper.getFunctionArgumentType(functionType, 0).getRawClass();
assertThat(functionInputClass).isNotNull();
assertThat(functionInputClass.getTypeName()).isEqualTo(WeatherRequest.class.getName());
}
public record WeatherRequest(String city) {
}
public record WeatherResponse(float temperatureInCelsius) {
}
public static class Outer {
public static class InnerWeatherFunction implements Function<WeatherRequest, WeatherResponse> {
@Override
public WeatherResponse apply(WeatherRequest weatherRequest) {
return new WeatherResponse(42.0f);
}
}
}
@Configuration
@ComponentScan({ "org.springframework.ai.tool.resolution.config",
"org.springframework.ai.tool.resolution.component" })
public static class TypeResolverHelperConfiguration {
@Bean
Outer.InnerWeatherFunction weatherClassDefinition() {
return new Outer.InnerWeatherFunction();
}
@Bean
Function<WeatherRequest, WeatherResponse> weatherFunctionDefinition() {
return new Outer.InnerWeatherFunction();
}
@Bean
StandaloneWeatherFunction standaloneWeatherFunction() {
return new StandaloneWeatherFunction();
}
@Bean
Consumer<WeatherRequest> weatherConsumer() {
return System.out::println;
}
}
}

View File

@@ -1,107 +0,0 @@
/*
* Copyright 2023-2025 the original author or authors.
*
* 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
*
* https://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 org.springframework.ai.tool.resolution;
import java.util.function.Consumer;
import java.util.function.Function;
import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import org.junit.jupiter.api.Test;
import org.springframework.ai.tool.resolution.TypeResolverHelperTests.MockWeatherService.Request;
import org.springframework.ai.tool.resolution.TypeResolverHelperTests.MockWeatherService.Response;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Christian Tzolov
*/
public class TypeResolverHelperTests {
@Test
public void testGetConsumerInputType() {
Class<?> inputType = TypeResolverHelper.getConsumerInputClass(MyConsumer.class);
assertThat(inputType).isEqualTo(Request.class);
}
@Test
public void testGetFunctionInputType() {
Class<?> inputType = TypeResolverHelper.getFunctionInputClass(MockWeatherService.class);
assertThat(inputType).isEqualTo(Request.class);
}
@Test
public void testGetFunctionOutputType() {
Class<?> outputType = TypeResolverHelper.getFunctionOutputClass(MockWeatherService.class);
assertThat(outputType).isEqualTo(Response.class);
}
@Test
public void testGetFunctionInputTypeForInstance() {
MockWeatherService service = new MockWeatherService();
Class<?> inputType = TypeResolverHelper.getFunctionInputClass(service.getClass());
assertThat(inputType).isEqualTo(Request.class);
}
public static class OutputFunctionConverter implements Function<Response, String> {
@Override
public String apply(Response response) {
return response.temp + " " + response.unit;
}
}
public static class MyConsumer implements Consumer<Request> {
@Override
public void accept(Request request) {
}
}
public static class MockWeatherService implements Function<Request, Response> {
@Override
public Response apply(Request request) {
return new Response(10, "C");
}
/**
* Weather Function request.
*/
@JsonInclude(Include.NON_NULL)
@JsonClassDescription("Weather API request")
public record Request(@JsonProperty(required = true,
value = "location") @JsonPropertyDescription("The city and state e.g. San Francisco, CA") String location,
@JsonProperty(required = true, value = "lat") @JsonPropertyDescription("The city latitude") double lat,
@JsonProperty(required = true, value = "lon") @JsonPropertyDescription("The city longitude") double lon,
@JsonProperty(required = true,
value = "unit") @JsonPropertyDescription("Temperature unit") String unit) {
}
public record Response(double temp, String unit) {
}
}
}

View File

@@ -1,36 +0,0 @@
/*
* Copyright 2023-2025 the original author or authors.
*
* 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
*
* https://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 org.springframework.ai.tool.resolution.component;
import java.util.function.Function;
import org.springframework.ai.tool.resolution.TypeResolverHelperIT.WeatherRequest;
import org.springframework.ai.tool.resolution.TypeResolverHelperIT.WeatherResponse;
import org.springframework.stereotype.Component;
/**
* @author Sebastien Deleuze
*/
@Component
public class ComponentWeatherFunction implements Function<WeatherRequest, WeatherResponse> {
@Override
public WeatherResponse apply(WeatherRequest weatherRequest) {
return new WeatherResponse(42.0f);
}
}

View File

@@ -1,31 +0,0 @@
/*
* Copyright 2023-2025 the original author or authors.
*
* 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
*
* https://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 org.springframework.ai.tool.resolution.config;
import org.springframework.ai.tool.resolution.StandaloneWeatherFunction;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TypeResolverHelperConfiguration {
@Bean
StandaloneWeatherFunction scannedStandaloneWeatherFunction() {
return new StandaloneWeatherFunction();
}
}

View File

@@ -1,204 +0,0 @@
package org.springframework.ai.tool.utils;
import org.junit.jupiter.api.Test;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.definition.ToolDefinition;
import org.springframework.ai.tool.execution.DefaultToolCallResultConverter;
import org.springframework.ai.tool.execution.ToolCallResultConverter;
import org.springframework.ai.tool.util.ToolUtils;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Unit tests for {@link ToolUtils}.
*
* @author Thomas Vitale
*/
class ToolUtilsTests {
@Test
void shouldDetectDuplicateToolNames() {
ToolCallback callback1 = new TestToolCallback("tool_a");
ToolCallback callback2 = new TestToolCallback("tool_a");
ToolCallback callback3 = new TestToolCallback("tool_b");
List<String> duplicates = ToolUtils.getDuplicateToolNames(callback1, callback2, callback3);
assertThat(duplicates).isNotEmpty();
assertThat(duplicates).contains("tool_a");
}
@Test
void shouldNotDetectDuplicateToolNames() {
ToolCallback callback1 = new TestToolCallback("tool_a");
ToolCallback callback2 = new TestToolCallback("tool_b");
ToolCallback callback3 = new TestToolCallback("tool_c");
List<String> duplicates = ToolUtils.getDuplicateToolNames(callback1, callback2, callback3);
assertThat(duplicates).isEmpty();
}
@Test
void shouldGetToolNameFromAnnotation() throws Exception {
Method method = TestTools.class.getMethod("toolWithCustomName");
assertThat(ToolUtils.getToolName(method)).isEqualTo("customName");
}
@Test
void shouldGetMethodNameWhenNoCustomNameInAnnotation() throws Exception {
Method method = TestTools.class.getMethod("toolWithoutCustomName");
assertThat(ToolUtils.getToolName(method)).isEqualTo("toolWithoutCustomName");
}
@Test
void shouldGetMethodNameWhenNoAnnotation() throws Exception {
Method method = TestTools.class.getMethod("methodWithoutAnnotation");
assertThat(ToolUtils.getToolName(method)).isEqualTo("methodWithoutAnnotation");
}
@Test
void shouldGetToolDescriptionFromAnnotation() throws Exception {
Method method = TestTools.class.getMethod("toolWithCustomDescription");
assertThat(ToolUtils.getToolDescription(method)).isEqualTo("Custom description");
}
@Test
void shouldGetToolDescriptionFromName() {
String description = ToolUtils.getToolDescriptionFromName("mySuperSpecialTool");
assertThat(description).isEqualTo("my super special tool");
}
@Test
void shouldGetMethodNameWhenNoCustomDescriptionInAnnotation() throws Exception {
Method method = TestTools.class.getMethod("toolWithoutCustomDescription");
assertThat(ToolUtils.getToolDescription(method)).isEqualTo("toolWithoutCustomDescription");
}
@Test
void shouldGetFormattedMethodNameWhenNoAnnotation() throws Exception {
Method method = TestTools.class.getMethod("camelCaseMethodWithoutAnnotation");
assertThat(ToolUtils.getToolDescription(method)).isEqualTo("camel case method without annotation");
}
@Test
void shouldGetToolReturnDirectFromAnnotation() throws Exception {
Method method = TestTools.class.getMethod("toolWithReturnDirect");
assertThat(ToolUtils.getToolReturnDirect(method)).isTrue();
}
@Test
void shouldGetDefaultReturnDirectWhenNoAnnotation() throws Exception {
Method method = TestTools.class.getMethod("methodWithoutAnnotation");
assertThat(ToolUtils.getToolReturnDirect(method)).isFalse();
}
@Test
void shouldGetToolCallResultConverterFromAnnotation() throws Exception {
Method method = TestTools.class.getMethod("toolWithCustomConverter");
ToolCallResultConverter converter = ToolUtils.getToolCallResultConverter(method);
assertThat(converter).isInstanceOf(CustomToolCallResultConverter.class);
}
@Test
void shouldGetDefaultConverterWhenNoAnnotation() throws Exception {
Method method = TestTools.class.getMethod("methodWithoutAnnotation");
ToolCallResultConverter converter = ToolUtils.getToolCallResultConverter(method);
assertThat(converter).isInstanceOf(DefaultToolCallResultConverter.class);
}
@Test
void shouldThrowExceptionWhenConverterCannotBeInstantiated() throws Exception {
Method method = TestTools.class.getMethod("toolWithInvalidConverter");
assertThatThrownBy(() -> ToolUtils.getToolCallResultConverter(method))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Failed to instantiate ToolCallResultConverter");
}
static class TestToolCallback implements ToolCallback {
private final ToolDefinition toolDefinition;
public TestToolCallback(String name) {
this.toolDefinition = ToolDefinition.builder().name(name).description(name).inputSchema("{}").build();
}
@Override
public ToolDefinition getToolDefinition() {
return toolDefinition;
}
@Override
public String call(String functionInput) {
return "";
}
}
static class TestTools {
@Tool(name = "customName")
public void toolWithCustomName() {
}
@Tool
public void toolWithoutCustomName() {
}
@Tool(description = "Custom description")
public void toolWithCustomDescription() {
}
@Tool
public void toolWithoutCustomDescription() {
}
@Tool(returnDirect = true)
public void toolWithReturnDirect() {
}
@Tool(resultConverter = CustomToolCallResultConverter.class)
public void toolWithCustomConverter() {
}
@Tool(resultConverter = InvalidToolCallResultConverter.class)
public void toolWithInvalidConverter() {
}
public void methodWithoutAnnotation() {
}
public void camelCaseMethodWithoutAnnotation() {
}
}
public static class CustomToolCallResultConverter implements ToolCallResultConverter {
@Override
public String convert(Object result, Type returnType) {
return returnType == null ? "null" : returnType.getTypeName();
}
}
// No-public class with no-public constructor
static class InvalidToolCallResultConverter implements ToolCallResultConverter {
private InvalidToolCallResultConverter() {
}
@Override
public String convert(Object result, Type returnType) {
return returnType == null ? "null" : returnType.getTypeName();
}
}
}

View File

@@ -74,6 +74,18 @@
<artifactId>reactor-core</artifactId>
</dependency>
<dependency>
<groupId>org.antlr</groupId>
<artifactId>ST4</artifactId>
<version>${ST4.version}</version>
</dependency>
<dependency>
<groupId>org.antlr</groupId>
<artifactId>antlr4-runtime</artifactId>
<version>${antlr.version}</version>
</dependency>
<dependency>
<groupId>com.github.victools</groupId>
<artifactId>jsonschema-generator</artifactId>
@@ -122,6 +134,19 @@
<artifactId>micrometer-observation-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.mockk</groupId>
<artifactId>mockk-jvm</artifactId>
<version>1.13.13</version>
<scope>test</scope>
</dependency>
</dependencies>

View File

@@ -28,7 +28,7 @@ import org.springframework.ai.chat.metadata.ChatGenerationMetadata.Builder;
* @since 1.0.0
*/
public class DefaultChatGenerationMetadataBuilder implements ChatGenerationMetadata.Builder {
public class DefaultChatGenerationMetadataBuilder implements Builder {
private String finishReason;

View File

@@ -66,8 +66,8 @@ public class ChatResponse implements ModelResponse<Generation> {
this.generations = List.copyOf(generations);
}
public static ChatResponse.Builder builder() {
return new ChatResponse.Builder();
public static Builder builder() {
return new Builder();
}
/**

View File

@@ -90,11 +90,10 @@ public interface ChatOptions extends ModelOptions {
<T extends ChatOptions> T copy();
/**
* Creates a new {@link ChatOptions.Builder} to create the default
* {@link ChatOptions}.
* @return Returns a new {@link ChatOptions.Builder}.
* Creates a new {@link Builder} to create the default {@link ChatOptions}.
* @return Returns a new {@link Builder}.
*/
static ChatOptions.Builder builder() {
static Builder builder() {
return new DefaultChatOptionsBuilder();
}

View File

@@ -164,7 +164,7 @@ public class DefaultFunctionCallbackBuilder implements FunctionCallback.Builder
}
final class DefaultMethodInvokingSpec extends DefaultCommonCallbackInvokingSpec<MethodInvokingSpec>
implements FunctionCallback.MethodInvokingSpec {
implements MethodInvokingSpec {
private String name;

View File

@@ -195,7 +195,7 @@ public class DefaultFunctionCallingOptions implements FunctionCallingOptions {
}
public FunctionCallingOptions merge(ChatOptions options) {
FunctionCallingOptions.Builder builder = FunctionCallingOptions.builder();
Builder builder = FunctionCallingOptions.builder();
builder.model(StringUtils.hasText(options.getModel()) ? options.getModel() : this.getModel())
.frequencyPenalty(
options.getFrequencyPenalty() != null ? options.getFrequencyPenalty() : this.getFrequencyPenalty())

View File

@@ -87,11 +87,11 @@ public interface FunctionCallback {
}
/**
* Creates a new {@link FunctionCallback.Builder} instance used to build a default
* Creates a new {@link Builder} instance used to build a default
* {@link FunctionCallback} instance.
* @return Returns a new {@link FunctionCallback.Builder} instance.
* @return Returns a new {@link Builder} instance.
*/
static FunctionCallback.Builder builder() {
static Builder builder() {
return new DefaultFunctionCallbackBuilder();
}

Some files were not shown because too many files have changed in this diff Show More