Advancing Tool Support - Part 2
* Enhanced support for functions as tools via FunctionToolCallback (deprecating the existing FunctionInvokingFunctionCallback). * Aligned JSON Schema generation and parsing logic between function-based and method-based tools. * Deprecated previous client-side function calling APIs. * Included AOT configuration for Tool-annotated methods in Spring beans. Relates to gh-2049 Signed-off-by: Thomas Vitale <ThomasVitale@users.noreply.github.com>
This commit is contained in:
committed by
Christian Tzolov
parent
39be9aeba2
commit
2f14597ccb
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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.aot;
|
||||
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.aot.generate.GenerationContext;
|
||||
import org.springframework.aot.hint.MemberCategory;
|
||||
import org.springframework.aot.hint.ReflectionHints;
|
||||
import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
|
||||
import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor;
|
||||
import org.springframework.beans.factory.aot.BeanRegistrationCode;
|
||||
import org.springframework.beans.factory.support.RegisteredBean;
|
||||
import org.springframework.core.annotation.MergedAnnotations;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.springframework.core.annotation.MergedAnnotations.SearchStrategy.TYPE_HIERARCHY;
|
||||
|
||||
/**
|
||||
* AOT {@code BeanRegistrationAotProcessor} that detects the presence of the {@link Tool}
|
||||
* annotation on methods and creates the required reflection hints.
|
||||
*
|
||||
* @author Thomas Vitale
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class ToolBeanRegistrationAotProcessor implements BeanRegistrationAotProcessor {
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) {
|
||||
Class<?> beanClass = registeredBean.getBeanClass();
|
||||
MergedAnnotations.Search search = MergedAnnotations.search(TYPE_HIERARCHY);
|
||||
|
||||
boolean hasAnyToolAnnotatedMethods = Stream.of(ReflectionUtils.getDeclaredMethods(beanClass))
|
||||
.anyMatch(method -> search.from(method).isPresent(Tool.class));
|
||||
|
||||
if (hasAnyToolAnnotatedMethods) {
|
||||
return new AotContribution(beanClass);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static class AotContribution implements BeanRegistrationAotContribution {
|
||||
|
||||
private final MemberCategory[] memberCategories = new MemberCategory[] { MemberCategory.INVOKE_DECLARED_METHODS,
|
||||
MemberCategory.INVOKE_PUBLIC_METHODS };
|
||||
|
||||
private final Class<?> toolClass;
|
||||
|
||||
public AotContribution(Class<?> toolClass) {
|
||||
this.toolClass = toolClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) {
|
||||
ReflectionHints reflectionHints = generationContext.getRuntimeHints().reflection();
|
||||
reflectionHints.registerType(toolClass, memberCategories);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@NonNullApi
|
||||
@NonNullFields
|
||||
package org.springframework.ai.aot;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
import org.springframework.lang.NonNullFields;
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -25,6 +25,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.springframework.ai.chat.model.ToolContext;
|
||||
import org.springframework.ai.tool.function.FunctionToolCallback;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -41,7 +42,9 @@ import org.springframework.util.Assert;
|
||||
* @param <I> the 3rd party service input type.
|
||||
* @param <O> the 3rd party service output type.
|
||||
* @author Christian Tzolov
|
||||
* @deprecated in favor of {@link FunctionToolCallback}.
|
||||
*/
|
||||
@Deprecated
|
||||
abstract class AbstractFunctionCallback<I, O> implements BiFunction<I, ToolContext, O>, FunctionCallback {
|
||||
|
||||
private final String name;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2024-2024 the original author or authors.
|
||||
* Copyright 2024-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.
|
||||
@@ -26,9 +26,16 @@ import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||
|
||||
import org.springframework.ai.model.function.FunctionCallback.CommonCallbackInvokingSpec;
|
||||
import org.springframework.ai.model.function.FunctionCallback.SchemaType;
|
||||
import org.springframework.ai.tool.function.FunctionToolCallback;
|
||||
import org.springframework.ai.tool.method.MethodToolCallback;
|
||||
import org.springframework.ai.util.JacksonUtils;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* @deprecated Use specific builder for the type of tool you need, e.g.
|
||||
* {@link FunctionToolCallback.Builder} and {@link MethodToolCallback.Builder}.
|
||||
*/
|
||||
@Deprecated
|
||||
public class DefaultCommonCallbackInvokingSpec<B extends CommonCallbackInvokingSpec<B>>
|
||||
implements CommonCallbackInvokingSpec<B> {
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -31,6 +31,8 @@ import org.springframework.ai.model.ModelOptionsUtils;
|
||||
import org.springframework.ai.model.function.FunctionCallback.FunctionInvokingSpec;
|
||||
import org.springframework.ai.model.function.FunctionCallback.MethodInvokingSpec;
|
||||
import org.springframework.ai.model.function.FunctionCallback.SchemaType;
|
||||
import org.springframework.ai.tool.function.FunctionToolCallback;
|
||||
import org.springframework.ai.tool.method.MethodToolCallback;
|
||||
import org.springframework.ai.util.ParsingUtils;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -42,7 +44,10 @@ import org.springframework.util.StringUtils;
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @since 1.0.0
|
||||
* @deprecated Use specific builder for the type of tool you need, e.g.
|
||||
* {@link FunctionToolCallback.Builder} and {@link MethodToolCallback.Builder}.
|
||||
*/
|
||||
@Deprecated
|
||||
public class DefaultFunctionCallbackBuilder implements FunctionCallback.Builder {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DefaultFunctionCallbackBuilder.class);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2024-2024 the original author or authors.
|
||||
* Copyright 2024-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.
|
||||
@@ -25,6 +25,7 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
import org.springframework.ai.model.tool.DefaultToolCallingChatOptions;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
@@ -35,7 +36,9 @@ import org.springframework.util.StringUtils;
|
||||
* @author Christian Tzolov
|
||||
* @author Thomas Vitale
|
||||
* @author Ilayaperumal Gopinathan
|
||||
* @deprecated in favor of {@link DefaultToolCallingChatOptions}.
|
||||
*/
|
||||
@Deprecated
|
||||
public class DefaultFunctionCallingOptions implements FunctionCallingOptions {
|
||||
|
||||
private List<FunctionCallback> functionCallbacks = new ArrayList<>();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2024-2024 the original author or authors.
|
||||
* Copyright 2024-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.
|
||||
@@ -22,6 +22,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.ai.model.tool.DefaultToolCallingChatOptions;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -30,7 +31,9 @@ import org.springframework.util.Assert;
|
||||
* @author Christian Tzolov
|
||||
* @author Thomas Vitale
|
||||
* @author Ilayaperumal Gopinathan
|
||||
* @deprecated in favor of {@link DefaultToolCallingChatOptions.Builder}.
|
||||
*/
|
||||
@Deprecated
|
||||
public class DefaultFunctionCallingOptionsBuilder implements FunctionCallingOptions.Builder {
|
||||
|
||||
private final DefaultFunctionCallingOptions options;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -24,6 +24,9 @@ import java.util.function.Supplier;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.springframework.ai.chat.model.ToolContext;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.function.FunctionToolCallback;
|
||||
import org.springframework.ai.tool.method.MethodToolCallback;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
|
||||
/**
|
||||
@@ -31,7 +34,9 @@ import org.springframework.core.ParameterizedTypeReference;
|
||||
* Models and called on prompts that trigger the function call.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @deprecated in favor of {@link ToolCallback}.
|
||||
*/
|
||||
@Deprecated
|
||||
public interface FunctionCallback {
|
||||
|
||||
/**
|
||||
@@ -115,7 +120,11 @@ public interface FunctionCallback {
|
||||
* <li>{@link FunctionInvokingSpec} - The function invoking builder interface.
|
||||
* <li>{@link MethodInvokingSpec} - The method invoking builder interface.
|
||||
* </ul>
|
||||
*
|
||||
* @deprecated Use specific builder for the type of tool you need, e.g.
|
||||
* {@link FunctionToolCallback.Builder} and {@link MethodToolCallback.Builder}.
|
||||
*/
|
||||
@Deprecated
|
||||
interface Builder {
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -21,6 +21,7 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
import org.springframework.ai.model.tool.ToolCallingChatOptions;
|
||||
|
||||
/**
|
||||
* FunctionCallingOptions is a set of options that can be used to configure the function
|
||||
@@ -28,7 +29,9 @@ import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @author Ilayaperumal Gopinathan
|
||||
* @deprecated in favor of {@link ToolCallingChatOptions}.
|
||||
*/
|
||||
@Deprecated
|
||||
public interface FunctionCallingOptions extends ChatOptions {
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -23,6 +23,7 @@ import java.util.function.Function;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.springframework.ai.chat.model.ToolContext;
|
||||
import org.springframework.ai.tool.function.FunctionToolCallback;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -34,7 +35,9 @@ import org.springframework.util.Assert;
|
||||
* @param <I> the input type
|
||||
* @param <O> the output type
|
||||
* @author Christian Tzolov
|
||||
* @deprecated in favor of {@link FunctionToolCallback}.
|
||||
*/
|
||||
@Deprecated
|
||||
public final class FunctionInvokingFunctionCallback<I, O> extends AbstractFunctionCallback<I, O> {
|
||||
|
||||
private final BiFunction<I, ToolContext, O> biFunction;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -34,6 +34,7 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.ai.chat.model.ToolContext;
|
||||
import org.springframework.ai.model.ModelOptionsUtils;
|
||||
import org.springframework.ai.tool.method.MethodToolCallback;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
@@ -51,7 +52,9 @@ import org.springframework.util.ReflectionUtils;
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @since 1.0.0
|
||||
* @deprecated in favor of {@link MethodToolCallback}.
|
||||
*/
|
||||
@Deprecated
|
||||
public class MethodInvokingFunctionCallback implements FunctionCallback {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(MethodInvokingFunctionCallback.class);
|
||||
|
||||
@@ -41,18 +41,21 @@ public interface ToolCallback extends FunctionCallback {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated // Call getToolDefinition().name() instead
|
||||
default String getName() {
|
||||
return getToolDefinition().name();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated // Call getToolDefinition().description() instead
|
||||
default String getDescription() {
|
||||
return getToolDefinition().description();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated // Call getToolDefinition().inputTypeSchema() instead
|
||||
default String getInputTypeSchema() {
|
||||
return getToolDefinition().inputTypeSchema();
|
||||
return getToolDefinition().inputSchema();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
|
||||
package org.springframework.ai.tool.definition;
|
||||
|
||||
import org.springframework.ai.tool.util.ToolUtils;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link ToolDefinition}.
|
||||
@@ -24,12 +26,12 @@ import org.springframework.util.Assert;
|
||||
* @author Thomas Vitale
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public record DefaultToolDefinition(String name, String description, String inputTypeSchema) implements ToolDefinition {
|
||||
public record DefaultToolDefinition(String name, String description, String inputSchema) implements ToolDefinition {
|
||||
|
||||
public DefaultToolDefinition {
|
||||
Assert.hasText(name, "name cannot be null or empty");
|
||||
Assert.hasText(description, "description cannot be null or empty");
|
||||
Assert.hasText(inputTypeSchema, "inputTypeSchema cannot be null or empty");
|
||||
Assert.hasText(inputSchema, "inputSchema cannot be null or empty");
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
@@ -42,7 +44,7 @@ public record DefaultToolDefinition(String name, String description, String inpu
|
||||
|
||||
private String description;
|
||||
|
||||
private String inputTypeSchema;
|
||||
private String inputSchema;
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
@@ -57,13 +59,16 @@ public record DefaultToolDefinition(String name, String description, String inpu
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder inputTypeSchema(String inputTypeSchema) {
|
||||
this.inputTypeSchema = inputTypeSchema;
|
||||
public Builder inputSchema(String inputSchema) {
|
||||
this.inputSchema = inputSchema;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DefaultToolDefinition build() {
|
||||
return new DefaultToolDefinition(name, description, inputTypeSchema);
|
||||
if (!StringUtils.hasText(description)) {
|
||||
description = ToolUtils.getToolDescriptionFromName(description);
|
||||
}
|
||||
return new DefaultToolDefinition(name, description, inputSchema);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -40,9 +40,9 @@ public interface ToolDefinition {
|
||||
String description();
|
||||
|
||||
/**
|
||||
* The JSON Schema of the parameters used to call the tool.
|
||||
* The schema of the parameters used to call the tool.
|
||||
*/
|
||||
String inputTypeSchema();
|
||||
String inputSchema();
|
||||
|
||||
/**
|
||||
* Create a default {@link ToolDefinition} builder.
|
||||
@@ -58,7 +58,7 @@ public interface ToolDefinition {
|
||||
return DefaultToolDefinition.builder()
|
||||
.name(ToolUtils.getToolName(method))
|
||||
.description(ToolUtils.getToolDescription(method))
|
||||
.inputTypeSchema(JsonSchemaGenerator.generateForMethodInput(method))
|
||||
.inputSchema(JsonSchemaGenerator.generateForMethodInput(method))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -16,9 +16,12 @@
|
||||
|
||||
package org.springframework.ai.tool.execution;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.ai.util.json.JsonParser;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
/**
|
||||
* A default implementation of {@link ToolCallResultConverter}.
|
||||
@@ -28,10 +31,12 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public final class DefaultToolCallResultConverter implements ToolCallResultConverter {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DefaultToolCallResultConverter.class);
|
||||
|
||||
@Override
|
||||
public String apply(@Nullable Object result, Class<?> returnType) {
|
||||
Assert.notNull(returnType, "returnType cannot be null");
|
||||
public String apply(@Nullable Object result, @Nullable Type returnType) {
|
||||
if (returnType == Void.TYPE) {
|
||||
logger.debug("The tool has no return type. Converting to conventional response.");
|
||||
return "Done";
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.springframework.ai.tool.execution;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
/**
|
||||
@@ -28,12 +29,12 @@ import java.util.function.BiFunction;
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ToolCallResultConverter extends BiFunction<Object, Class<?>, String> {
|
||||
public interface ToolCallResultConverter extends BiFunction<Object, Type, String> {
|
||||
|
||||
/**
|
||||
* Given an Object returned by a tool, convert it to a String compatible with the
|
||||
* given class type.
|
||||
*/
|
||||
String apply(@Nullable Object result, Class<?> returnType);
|
||||
String apply(@Nullable Object result, @Nullable Type returnType);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* 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.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
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.DefaultToolCallResultConverter;
|
||||
import org.springframework.ai.tool.execution.ToolCallResultConverter;
|
||||
import org.springframework.ai.tool.metadata.ToolMetadata;
|
||||
import org.springframework.ai.tool.util.ToolUtils;
|
||||
import org.springframework.ai.util.json.JsonParser;
|
||||
import org.springframework.ai.util.json.JsonSchemaGenerator;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* A {@link ToolCallback} implementation to invoke functions as tools.
|
||||
*
|
||||
* @author Thomas Vitale
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class FunctionToolCallback<I, O> implements ToolCallback {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(FunctionToolCallback.class);
|
||||
|
||||
private static final ToolCallResultConverter DEFAULT_RESULT_CONVERTER = new DefaultToolCallResultConverter();
|
||||
|
||||
private static final ToolMetadata DEFAULT_TOOL_METADATA = ToolMetadata.builder().build();
|
||||
|
||||
private final ToolDefinition toolDefinition;
|
||||
|
||||
private final ToolMetadata toolMetadata;
|
||||
|
||||
private final Type toolInputType;
|
||||
|
||||
private final BiFunction<I, ToolContext, O> toolFunction;
|
||||
|
||||
private final ToolCallResultConverter toolCallResultConverter;
|
||||
|
||||
public FunctionToolCallback(ToolDefinition toolDefinition, @Nullable ToolMetadata toolMetadata, Type toolInputType,
|
||||
BiFunction<I, ToolContext, O> toolFunction, @Nullable ToolCallResultConverter toolCallResultConverter) {
|
||||
Assert.notNull(toolDefinition, "toolDefinition cannot be null");
|
||||
Assert.notNull(toolInputType, "toolInputType cannot be null");
|
||||
Assert.notNull(toolFunction, "toolFunction cannot be null");
|
||||
this.toolDefinition = toolDefinition;
|
||||
this.toolMetadata = toolMetadata != null ? toolMetadata : DEFAULT_TOOL_METADATA;
|
||||
this.toolFunction = toolFunction;
|
||||
this.toolInputType = toolInputType;
|
||||
this.toolCallResultConverter = toolCallResultConverter != null ? toolCallResultConverter
|
||||
: DEFAULT_RESULT_CONVERTER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ToolDefinition getToolDefinition() {
|
||||
return toolDefinition;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ToolMetadata getToolMetadata() {
|
||||
return toolMetadata;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String call(String toolInput) {
|
||||
return call(toolInput, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String call(String toolInput, @Nullable ToolContext toolContext) {
|
||||
Assert.hasText(toolInput, "toolInput cannot be null or empty");
|
||||
|
||||
logger.debug("Starting execution of tool: {}", toolDefinition.name());
|
||||
|
||||
I request = JsonParser.fromJson(toolInput, toolInputType);
|
||||
O response = toolFunction.apply(request, toolContext);
|
||||
|
||||
logger.debug("Successful execution of tool: {}", toolDefinition.name());
|
||||
|
||||
return toolCallResultConverter.apply(response, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a {@link FunctionToolCallback} from a {@link BiFunction}.
|
||||
*/
|
||||
public static <I, O> Builder<I, O> builder(String name, BiFunction<I, ToolContext, O> function) {
|
||||
return new Builder<>(name, function);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a {@link FunctionToolCallback} from a {@link Function}.
|
||||
*/
|
||||
public static <I, O> Builder<I, O> builder(String name, Function<I, O> function) {
|
||||
Assert.notNull(function, "function cannot be null");
|
||||
return new Builder<>(name, (request, context) -> function.apply(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a {@link FunctionToolCallback} from a {@link Supplier}.
|
||||
*/
|
||||
public static <O> Builder<Void, O> builder(String name, Supplier<O> supplier) {
|
||||
Assert.notNull(supplier, "supplier cannot be null");
|
||||
Function<Void, O> function = input -> supplier.get();
|
||||
return builder(name, function).inputType(Void.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a {@link FunctionToolCallback} from a {@link Consumer}.
|
||||
*/
|
||||
public static <I> Builder<I, Void> builder(String name, Consumer<I> consumer) {
|
||||
Assert.notNull(consumer, "consumer cannot be null");
|
||||
Function<I, Void> function = (I input) -> {
|
||||
consumer.accept(input);
|
||||
return null;
|
||||
};
|
||||
return builder(name, function);
|
||||
}
|
||||
|
||||
public static class Builder<I, O> {
|
||||
|
||||
private String name;
|
||||
|
||||
private String description;
|
||||
|
||||
private String inputSchema;
|
||||
|
||||
private Type inputType;
|
||||
|
||||
private ToolMetadata toolMetadata;
|
||||
|
||||
private BiFunction<I, ToolContext, O> toolFunction;
|
||||
|
||||
private ToolCallResultConverter toolCallResultConverter;
|
||||
|
||||
private Builder(String name, BiFunction<I, ToolContext, O> toolFunction) {
|
||||
Assert.hasText(name, "name cannot be null or empty");
|
||||
Assert.notNull(toolFunction, "toolFunction cannot be null");
|
||||
this.name = name;
|
||||
this.toolFunction = toolFunction;
|
||||
}
|
||||
|
||||
public Builder<I, O> description(String description) {
|
||||
this.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder<I, O> inputSchema(String inputSchema) {
|
||||
this.inputSchema = inputSchema;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder<I, O> inputType(Type inputType) {
|
||||
this.inputType = inputType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder<I, O> inputType(ParameterizedTypeReference<?> inputType) {
|
||||
Assert.notNull(inputType, "inputType cannot be null");
|
||||
this.inputType = inputType.getType();
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder<I, O> toolMetadata(ToolMetadata toolMetadata) {
|
||||
this.toolMetadata = toolMetadata;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder<I, O> toolCallResultConverter(ToolCallResultConverter toolCallResultConverter) {
|
||||
this.toolCallResultConverter = toolCallResultConverter;
|
||||
return this;
|
||||
}
|
||||
|
||||
public FunctionToolCallback<I, O> build() {
|
||||
Assert.notNull(inputType, "inputType cannot be null");
|
||||
var toolDefinition = ToolDefinition.builder()
|
||||
.name(name)
|
||||
.description(
|
||||
StringUtils.hasText(description) ? description : ToolUtils.getToolDescriptionFromName(name))
|
||||
.inputSchema(
|
||||
StringUtils.hasText(inputSchema) ? inputSchema : JsonSchemaGenerator.generateForType(inputType))
|
||||
.build();
|
||||
return new FunctionToolCallback<>(toolDefinition, toolMetadata, inputType, toolFunction,
|
||||
toolCallResultConverter);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@NonNullApi
|
||||
@NonNullFields
|
||||
package org.springframework.ai.tool.function;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
import org.springframework.lang.NonNullFields;
|
||||
@@ -35,6 +35,7 @@ import org.springframework.util.CollectionUtils;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -50,6 +51,8 @@ public class MethodToolCallback implements ToolCallback {
|
||||
|
||||
private static final ToolCallResultConverter DEFAULT_RESULT_CONVERTER = new DefaultToolCallResultConverter();
|
||||
|
||||
private static final ToolMetadata DEFAULT_TOOL_METADATA = ToolMetadata.builder().build();
|
||||
|
||||
private final ToolDefinition toolDefinition;
|
||||
|
||||
private final ToolMetadata toolMetadata;
|
||||
@@ -60,14 +63,13 @@ public class MethodToolCallback implements ToolCallback {
|
||||
|
||||
private final ToolCallResultConverter toolCallResultConverter;
|
||||
|
||||
public MethodToolCallback(ToolDefinition toolDefinition, ToolMetadata toolMetadata, Method toolMethod,
|
||||
public MethodToolCallback(ToolDefinition toolDefinition, @Nullable ToolMetadata toolMetadata, Method toolMethod,
|
||||
Object toolObject, @Nullable ToolCallResultConverter toolCallResultConverter) {
|
||||
Assert.notNull(toolDefinition, "toolDefinition cannot be null");
|
||||
Assert.notNull(toolMetadata, "toolMetadata cannot be null");
|
||||
Assert.notNull(toolMethod, "toolMethod cannot be null");
|
||||
Assert.notNull(toolObject, "toolObject cannot be null");
|
||||
this.toolDefinition = toolDefinition;
|
||||
this.toolMetadata = toolMetadata;
|
||||
this.toolMetadata = toolMetadata != null ? toolMetadata : DEFAULT_TOOL_METADATA;
|
||||
this.toolMethod = toolMethod;
|
||||
this.toolObject = toolObject;
|
||||
this.toolCallResultConverter = toolCallResultConverter != null ? toolCallResultConverter
|
||||
@@ -105,7 +107,7 @@ public class MethodToolCallback implements ToolCallback {
|
||||
|
||||
logger.debug("Successful execution of tool: {}", toolDefinition.name());
|
||||
|
||||
Class<?> returnType = toolMethod.getReturnType();
|
||||
Type returnType = toolMethod.getGenericReturnType();
|
||||
|
||||
return toolCallResultConverter.apply(result, returnType);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.execution.DefaultToolCallResultConverter;
|
||||
import org.springframework.ai.tool.execution.ToolCallResultConverter;
|
||||
import org.springframework.ai.util.ParsingUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
@@ -47,6 +49,11 @@ public final class ToolUtils {
|
||||
return StringUtils.hasText(tool.name()) ? tool.name() : method.getName();
|
||||
}
|
||||
|
||||
public static String getToolDescriptionFromName(@Nullable String toolName) {
|
||||
Assert.hasText(toolName, "toolName cannot be null or empty");
|
||||
return ParsingUtils.reConcatenateCamelCase(toolName, " ");
|
||||
}
|
||||
|
||||
public static String getToolDescription(Method method) {
|
||||
var tool = method.getAnnotation(Tool.class);
|
||||
if (tool == null) {
|
||||
|
||||
@@ -27,6 +27,8 @@ import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
/**
|
||||
* Utilities to perform parsing operations between JSON and Java.
|
||||
*/
|
||||
@@ -64,6 +66,21 @@ public final class JsonParser {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a JSON string to a Java object.
|
||||
*/
|
||||
public static <T> T fromJson(String json, Type type) {
|
||||
Assert.notNull(json, "json cannot be null");
|
||||
Assert.notNull(type, "type cannot be null");
|
||||
|
||||
try {
|
||||
return OBJECT_MAPPER.readValue(json, OBJECT_MAPPER.constructType(type));
|
||||
}
|
||||
catch (JsonProcessingException ex) {
|
||||
throw new IllegalStateException("Conversion from JSON to %s failed".formatted(type.getTypeName()), ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a JSON string to a Java object.
|
||||
*/
|
||||
|
||||
@@ -112,6 +112,9 @@ public final class JsonSchemaGenerator {
|
||||
public static String generateForType(Type type, SchemaOption... schemaOptions) {
|
||||
Assert.notNull(type, "type cannot be null");
|
||||
ObjectNode schema = TYPE_SCHEMA_GENERATOR.generateSchema(type);
|
||||
if ((type == Void.class) && !schema.has("properties")) {
|
||||
schema.putObject("properties");
|
||||
}
|
||||
if (Stream.of(schemaOptions)
|
||||
.noneMatch(option -> option == SchemaOption.ALLOW_ADDITIONAL_PROPERTIES_BY_DEFAULT)) {
|
||||
schema.put("additionalProperties", false);
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
org.springframework.aot.hint.RuntimeHintsRegistrar=\
|
||||
org.springframework.ai.aot.SpringAiCoreRuntimeHints,\
|
||||
org.springframework.ai.aot.KnuddelsRuntimeHints,\
|
||||
org.springframework.ai.aot.ToolRuntimeHints
|
||||
org.springframework.ai.aot.ToolRuntimeHints
|
||||
|
||||
org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\
|
||||
org.springframework.ai.aot.ToolBeanRegistrationAotProcessor
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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.aot;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.aot.generate.GenerationContext;
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.RegisteredBean;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.reflection;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ToolBeanRegistrationAotProcessor}.
|
||||
*
|
||||
* @author Thomas Vitale
|
||||
*/
|
||||
class ToolBeanRegistrationAotProcessorTests {
|
||||
|
||||
private final GenerationContext generationContext = mock();
|
||||
|
||||
private final RuntimeHints runtimeHints = new RuntimeHints();
|
||||
|
||||
@Test
|
||||
void shouldSkipNonAnnotatedClass() {
|
||||
process(NonTools.class);
|
||||
assertThat(this.runtimeHints.reflection().typeHints()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldProcessAnnotatedClass() {
|
||||
process(TestTools.class);
|
||||
assertThat(reflection().onType(TestTools.class)).accepts(this.runtimeHints);
|
||||
}
|
||||
|
||||
private void process(Class<?> beanClass) {
|
||||
when(generationContext.getRuntimeHints()).thenReturn(runtimeHints);
|
||||
BeanRegistrationAotContribution contribution = createContribution(beanClass);
|
||||
if (contribution != null) {
|
||||
contribution.applyTo(this.generationContext, mock());
|
||||
}
|
||||
}
|
||||
|
||||
private static @Nullable BeanRegistrationAotContribution createContribution(Class<?> beanClass) {
|
||||
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
|
||||
beanFactory.registerBeanDefinition(beanClass.getName(), new RootBeanDefinition(beanClass));
|
||||
return new ToolBeanRegistrationAotProcessor()
|
||||
.processAheadOfTime(RegisteredBean.of(beanFactory, beanClass.getName()));
|
||||
}
|
||||
|
||||
static class TestTools {
|
||||
|
||||
@Tool
|
||||
String testTool() {
|
||||
return "Testing";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class NonTools {
|
||||
|
||||
String nonTool() {
|
||||
return "More testing";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -24,7 +24,7 @@ class ToolCallbackTests {
|
||||
private final ToolDefinition toolDefinition;
|
||||
|
||||
public TestToolCallback(String name) {
|
||||
this.toolDefinition = ToolDefinition.builder().name(name).description(name).inputTypeSchema("{}").build();
|
||||
this.toolDefinition = ToolDefinition.builder().name(name).description(name).inputSchema("{}").build();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -17,7 +17,7 @@ class DefaultToolDefinitionTests {
|
||||
var toolDefinition = new DefaultToolDefinition("name", "description", "{}");
|
||||
assertThat(toolDefinition.name()).isEqualTo("name");
|
||||
assertThat(toolDefinition.description()).isEqualTo("description");
|
||||
assertThat(toolDefinition.inputTypeSchema()).isEqualTo("{}");
|
||||
assertThat(toolDefinition.inputSchema()).isEqualTo("{}");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -49,17 +49,17 @@ class DefaultToolDefinitionTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowExceptionWhenInputTypeSchemaIsNull() {
|
||||
void shouldThrowExceptionWhenInputSchemaIsNull() {
|
||||
assertThatThrownBy(() -> new DefaultToolDefinition("name", "description", null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("inputTypeSchema cannot be null or empty");
|
||||
.hasMessage("inputSchema cannot be null or empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowExceptionWhenInputTypeSchemaIsEmpty() {
|
||||
void shouldThrowExceptionWhenInputSchemaIsEmpty() {
|
||||
assertThatThrownBy(() -> new DefaultToolDefinition("name", "description", ""))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("inputTypeSchema cannot be null or empty");
|
||||
.hasMessage("inputSchema cannot be null or empty");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,14 +16,10 @@ class ToolDefinitionTests {
|
||||
|
||||
@Test
|
||||
void shouldCreateDefaultToolDefinitionBuilder() {
|
||||
var toolDefinition = ToolDefinition.builder()
|
||||
.name("name")
|
||||
.description("description")
|
||||
.inputTypeSchema("{}")
|
||||
.build();
|
||||
var toolDefinition = ToolDefinition.builder().name("name").description("description").inputSchema("{}").build();
|
||||
assertThat(toolDefinition.name()).isEqualTo("name");
|
||||
assertThat(toolDefinition.description()).isEqualTo("description");
|
||||
assertThat(toolDefinition.inputTypeSchema()).isEqualTo("{}");
|
||||
assertThat(toolDefinition.inputSchema()).isEqualTo("{}");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -31,7 +27,7 @@ class ToolDefinitionTests {
|
||||
var toolDefinition = ToolDefinition.from(Tools.class.getDeclaredMethods()[0]);
|
||||
assertThat(toolDefinition.name()).isEqualTo("mySuperTool");
|
||||
assertThat(toolDefinition.description()).isEqualTo("Test description");
|
||||
assertThat(toolDefinition.inputTypeSchema()).isEqualToIgnoringWhitespace("""
|
||||
assertThat(toolDefinition.inputSchema()).isEqualToIgnoringWhitespace("""
|
||||
{
|
||||
"$schema" : "https://json-schema.org/draft/2020-12/schema",
|
||||
"type" : "object",
|
||||
|
||||
@@ -6,7 +6,6 @@ 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 DefaultToolCallResultConverter}.
|
||||
@@ -18,9 +17,9 @@ class DefaultToolCallResultConverterTests {
|
||||
private final DefaultToolCallResultConverter converter = new DefaultToolCallResultConverter();
|
||||
|
||||
@Test
|
||||
void convertWithNullReturnTypeShouldThrowException() {
|
||||
assertThatThrownBy(() -> converter.apply(null, null)).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("returnType cannot be null");
|
||||
void convertWithNullReturnTypeShouldReturn() {
|
||||
String result = converter.apply(null, null);
|
||||
assertThat(result).isEqualTo("null");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
* 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.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) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,6 +9,7 @@ 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;
|
||||
@@ -68,6 +69,12 @@ class ToolUtilsTests {
|
||||
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");
|
||||
@@ -119,7 +126,7 @@ class ToolUtilsTests {
|
||||
private final ToolDefinition toolDefinition;
|
||||
|
||||
public TestToolCallback(String name) {
|
||||
this.toolDefinition = ToolDefinition.builder().name(name).description(name).inputTypeSchema("{}").build();
|
||||
this.toolDefinition = ToolDefinition.builder().name(name).description(name).inputSchema("{}").build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -175,8 +182,8 @@ class ToolUtilsTests {
|
||||
public static class CustomToolCallResultConverter implements ToolCallResultConverter {
|
||||
|
||||
@Override
|
||||
public String apply(Object result, Class<?> returnType) {
|
||||
return returnType.getName();
|
||||
public String apply(Object result, Type returnType) {
|
||||
return returnType == null ? "null" : returnType.getTypeName();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -188,8 +195,8 @@ class ToolUtilsTests {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String apply(Object result, Class<?> returnType) {
|
||||
return returnType.getName();
|
||||
public String apply(Object result, Type returnType) {
|
||||
return returnType == null ? "null" : returnType.getTypeName();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package org.springframework.ai.util.json;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
@@ -108,6 +110,20 @@ class JsonParserTests {
|
||||
assertThat(object.name).isEqualTo("James");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromJsonToObjectWithType() {
|
||||
var json = """
|
||||
{
|
||||
"name" : "John",
|
||||
"age" : 30
|
||||
}
|
||||
""";
|
||||
TestRecord object = JsonParser.fromJson(json, (Type) TestRecord.class);
|
||||
assertThat(object).isNotNull();
|
||||
assertThat(object.name).isEqualTo("John");
|
||||
assertThat(object.age).isEqualTo(30);
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromObjectToJson() {
|
||||
var object = new TestRecord("John", 30);
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
* 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.integration.tests.tool;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.integration.tests.TestApplication;
|
||||
import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import org.springframework.ai.tool.function.FunctionToolCallback;
|
||||
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.Configuration;
|
||||
import org.springframework.context.annotation.Description;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link FunctionToolCallback}.
|
||||
*
|
||||
* @author Thomas Vitale
|
||||
*/
|
||||
@SpringBootTest(classes = TestApplication.class)
|
||||
@Import(FunctionToolCallbackTests.Tools.class)
|
||||
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*")
|
||||
public class FunctionToolCallbackTests {
|
||||
|
||||
// @formatter:off
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(FunctionToolCallbackTests.class);
|
||||
|
||||
@Autowired
|
||||
OpenAiChatModel openAiChatModel;
|
||||
|
||||
@Test
|
||||
void chatVoidInputFromBean() {
|
||||
var content = ChatClient.builder(this.openAiChatModel)
|
||||
.build()
|
||||
.prompt()
|
||||
.user("Welcome the users to the library")
|
||||
.tools(Tools.WELCOME)
|
||||
.call()
|
||||
.content();
|
||||
assertThat(content).isNotEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void chatVoidInputFromCallback() {
|
||||
var content = ChatClient.builder(this.openAiChatModel)
|
||||
.build()
|
||||
.prompt()
|
||||
.user("Welcome the users to the library")
|
||||
.toolCallbacks(FunctionToolCallback.builder("sayWelcome", (input) -> {
|
||||
logger.info("CALLBACK - Welcoming users to the library");
|
||||
})
|
||||
.description("Welcome users to the library")
|
||||
.inputType(Void.class)
|
||||
.build())
|
||||
.call()
|
||||
.content();
|
||||
assertThat(content).isNotEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void chatVoidOutputFromBean() {
|
||||
var content = ChatClient.builder(this.openAiChatModel)
|
||||
.build()
|
||||
.prompt()
|
||||
.user("Welcome %s to the library".formatted("James Bond"))
|
||||
.tools(Tools.WELCOME_USER)
|
||||
.call()
|
||||
.content();
|
||||
assertThat(content).isNotEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void chatVoidOutputFromCallback() {
|
||||
var content = ChatClient.builder(this.openAiChatModel)
|
||||
.build()
|
||||
.prompt()
|
||||
.user("Welcome %s to the library".formatted("James Bond"))
|
||||
.toolCallbacks(FunctionToolCallback.builder("welcomeUser", (user) -> {
|
||||
logger.info("CALLBACK - Welcoming {} to the library", ((User) user).name());
|
||||
})
|
||||
.description("Welcome a specific user to the library")
|
||||
.inputType(User.class)
|
||||
.build())
|
||||
.call()
|
||||
.content();
|
||||
assertThat(content).contains("Bond");
|
||||
}
|
||||
|
||||
@Test
|
||||
void chatSingleFromBean() {
|
||||
var content = ChatClient.builder(this.openAiChatModel)
|
||||
.build()
|
||||
.prompt()
|
||||
.user("What books written by %s are available in the library?".formatted("J.R.R. Tolkien"))
|
||||
.tools(Tools.BOOKS_BY_AUTHOR)
|
||||
.call()
|
||||
.content();
|
||||
assertThat(content).isNotEmpty()
|
||||
.contains("The Hobbit")
|
||||
.contains("The Lord of The Rings")
|
||||
.contains("The Silmarillion");
|
||||
}
|
||||
|
||||
@Test
|
||||
void chatSingleFromCallback() {
|
||||
Function<Author, List<Book>> function = author -> {
|
||||
logger.info("CALLBACK - Getting books by author: {}", author.name());
|
||||
return new BookService().getBooksByAuthor(author);
|
||||
};
|
||||
var content = ChatClient.builder(this.openAiChatModel)
|
||||
.build()
|
||||
.prompt()
|
||||
.user("What books written by %s are available in the library?".formatted("J.R.R. Tolkien"))
|
||||
.toolCallbacks(FunctionToolCallback.builder("availableBooksByAuthor", function)
|
||||
.description("Get the list of books written by the given author available in the library")
|
||||
.inputType(Author.class)
|
||||
.build())
|
||||
.call()
|
||||
.content();
|
||||
assertThat(content).isNotEmpty()
|
||||
.contains("The Hobbit")
|
||||
.contains("The Lord of The Rings")
|
||||
.contains("The Silmarillion");
|
||||
}
|
||||
|
||||
@Test
|
||||
void chatListFromBean() {
|
||||
var content = ChatClient.builder(this.openAiChatModel)
|
||||
.build()
|
||||
.prompt()
|
||||
.user("What authors wrote the books %s and %s available in the library?".formatted("The Hobbit", "Narnia"))
|
||||
.tools(Tools.AUTHORS_BY_BOOKS)
|
||||
.call()
|
||||
.content();
|
||||
assertThat(content).isNotEmpty().contains("J.R.R. Tolkien").contains("C.S. Lewis");
|
||||
}
|
||||
|
||||
@Test
|
||||
void chatListFromCallback() {
|
||||
Function<Books, List<Author>> function = books -> {
|
||||
logger.info("CALLBACK - Getting authors by books: {}", books.books().stream().map(Book::title).toList());
|
||||
return new BookService().getAuthorsByBook(books.books());
|
||||
};
|
||||
var content = ChatClient.builder(this.openAiChatModel)
|
||||
.build()
|
||||
.prompt()
|
||||
.user("What authors wrote the books %s and %s available in the library?".formatted("The Hobbit", "Narnia"))
|
||||
.toolCallbacks(FunctionToolCallback.builder("authorsByAvailableBooks", function)
|
||||
.description("Get the list of authors who wrote the given books available in the library")
|
||||
.inputType(Books.class)
|
||||
.build())
|
||||
.call()
|
||||
.content();
|
||||
assertThat(content).isNotEmpty().contains("J.R.R. Tolkien").contains("C.S. Lewis");
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class Tools {
|
||||
|
||||
public static final String AUTHORS_BY_BOOKS = "authorsByBooks";
|
||||
|
||||
public static final String BOOKS_BY_AUTHOR = "booksByAuthor";
|
||||
|
||||
public static final String WELCOME = "welcome";
|
||||
|
||||
public static final String WELCOME_USER = "welcomeUser";
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(Tools.class);
|
||||
|
||||
private final BookService bookService = new BookService();
|
||||
|
||||
@Bean(WELCOME)
|
||||
@Description("Welcome users to the library")
|
||||
Consumer<Void> welcome() {
|
||||
return (input) -> logger.info("Welcoming users to the library");
|
||||
}
|
||||
|
||||
@Bean(WELCOME_USER)
|
||||
@Description("Welcome a specific user to the library")
|
||||
Consumer<User> welcomeUser() {
|
||||
return user -> logger.info("Welcoming {} to the library", user.name());
|
||||
}
|
||||
|
||||
@Bean(BOOKS_BY_AUTHOR)
|
||||
@Description("Get the list of books written by the given author available in the library")
|
||||
Function<Author, List<Book>> booksByAuthor() {
|
||||
return author -> {
|
||||
logger.info("Getting books by author: {}", author.name());
|
||||
return bookService.getBooksByAuthor(author);
|
||||
};
|
||||
}
|
||||
|
||||
@Bean(AUTHORS_BY_BOOKS)
|
||||
@Description("Get the list of authors who wrote the given books available in the library")
|
||||
Function<Books, List<Author>> authorsByBooks() {
|
||||
return books -> {
|
||||
logger.info("Getting authors by books: {}", books.books().stream().map(Book::title).toList());
|
||||
return bookService.getAuthorsByBook(books.books());
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public record User(String name) {
|
||||
}
|
||||
|
||||
public record Author(String name) {
|
||||
}
|
||||
|
||||
public record Authors(List<Author> authors) {
|
||||
}
|
||||
|
||||
public record Book(String title, String author) {
|
||||
}
|
||||
|
||||
public record Books(List<Book> books) {
|
||||
}
|
||||
|
||||
static class BookService {
|
||||
|
||||
private static final Map<Integer, Book> books = new ConcurrentHashMap<>();
|
||||
|
||||
static {
|
||||
books.put(1, new Book("His Dark Materials", "Philip Pullman"));
|
||||
books.put(2, new Book("Narnia", "C.S. Lewis"));
|
||||
books.put(3, new Book("The Hobbit", "J.R.R. Tolkien"));
|
||||
books.put(4, new Book("The Lord of The Rings", "J.R.R. Tolkien"));
|
||||
books.put(5, new Book("The Silmarillion", "J.R.R. Tolkien"));
|
||||
}
|
||||
|
||||
public List<Book> getBooksByAuthor(Author author) {
|
||||
return books.values().stream().filter(book -> author.name().equals(book.author())).toList();
|
||||
}
|
||||
|
||||
public List<Author> getAuthorsByBook(List<Book> booksToSearch) {
|
||||
return books.values()
|
||||
.stream()
|
||||
.filter(book -> booksToSearch.stream().anyMatch(b -> b.title().equals(book.title())))
|
||||
.map(book -> new Author(book.author()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// @formatter:on
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user