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:
committed by
Soby Chacko
parent
7f3852ee4d
commit
53af6fd5ac
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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 "";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ public class DefaultFunctionCallbackBuilder implements FunctionCallback.Builder
|
||||
}
|
||||
|
||||
final class DefaultMethodInvokingSpec extends DefaultCommonCallbackInvokingSpec<MethodInvokingSpec>
|
||||
implements FunctionCallback.MethodInvokingSpec {
|
||||
implements MethodInvokingSpec {
|
||||
|
||||
private String name;
|
||||
|
||||
@@ -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())
|
||||
@@ -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
Reference in New Issue
Block a user