Refactor the Function Calling Support
- Remove the SpringAiFunction annotation in favour of palin Functino Beans, @Description annotation and JacksonClassAnnotation. - Update the function calling documentation to reflect latest changes. - Add a new openai option (and related property): spring.ai.openai.chat.options.beanFunctions.<function-name>.<description> Map of bean names and their descriptions to register as function callbacks. - Refactor the OpenAiAutoConfiguration to resolve and register the beans in beanFunctions. - Add dependency on Spring Cloud Function to use the FunctionContextUtils and FunctionTypeUtils Those utilites help to resolve the function input type signature. - Add DefaultToolFunctionCallback class for manually wrapping Functions. - Update the ITs.
This commit is contained in:
@@ -37,7 +37,7 @@ import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
|
||||
import org.springframework.ai.chat.metadata.RateLimit;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.model.ModelOptionsUtils;
|
||||
import org.springframework.ai.model.ToolFunctionCallback;
|
||||
import org.springframework.ai.model.function.ToolFunctionCallback;
|
||||
import org.springframework.ai.openai.api.OpenAiApi;
|
||||
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletion;
|
||||
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletionMessage;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.ai.openai;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -28,9 +29,10 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.springframework.ai.chat.ChatOptions;
|
||||
import org.springframework.ai.model.ToolFunctionCallback;
|
||||
import org.springframework.ai.model.function.ToolFunctionCallback;
|
||||
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletionRequest.ResponseFormat;
|
||||
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletionRequest.ToolChoice;
|
||||
import org.springframework.boot.context.properties.NestedConfigurationProperty;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.ai.openai.api.OpenAiApi.FunctionTool;
|
||||
|
||||
@@ -89,6 +91,7 @@ public class OpenAiChatOptions implements ChatOptions {
|
||||
/**
|
||||
* Up to 4 sequences where the API will stop generating further tokens.
|
||||
*/
|
||||
@NestedConfigurationProperty
|
||||
private @JsonProperty("stop") List<String> stop;
|
||||
/**
|
||||
* What sampling temperature to use, between 0 and 1. Higher values like 0.8 will make the output
|
||||
@@ -106,6 +109,7 @@ public class OpenAiChatOptions implements ChatOptions {
|
||||
* A list of tools the model may call. Currently, only functions are supported as a tool. Use this to
|
||||
* provide a list of functions the model may generate JSON inputs for.
|
||||
*/
|
||||
@NestedConfigurationProperty
|
||||
private @JsonProperty("tools") List<FunctionTool> tools;
|
||||
/**
|
||||
* Controls which (if any) function is called by the model. none means the model will not call a
|
||||
@@ -114,6 +118,7 @@ public class OpenAiChatOptions implements ChatOptions {
|
||||
* the model to call that function. none is the default when no functions are present. auto is the default if
|
||||
* functions are present.
|
||||
*/
|
||||
@NestedConfigurationProperty
|
||||
private @JsonProperty("tool_choice") ToolChoice toolChoice;
|
||||
/**
|
||||
* A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse.
|
||||
@@ -126,6 +131,7 @@ public class OpenAiChatOptions implements ChatOptions {
|
||||
* For Default Options the toolCallbacks are registered but disabled by default. Use the enableFunctions to set the functions
|
||||
* from the registry to be used by the ChatClient chat completion requests.
|
||||
*/
|
||||
@NestedConfigurationProperty
|
||||
@JsonIgnore
|
||||
private List<ToolFunctionCallback> toolCallbacks = new ArrayList<>();
|
||||
|
||||
@@ -138,8 +144,20 @@ public class OpenAiChatOptions implements ChatOptions {
|
||||
* Note that function enabled with the default options are enabled for all chat completion requests. This could impact the token count and the billing.
|
||||
* If the enabledFunctions is set in a prompt options, then the enabled functions are only active for the duration of this prompt execution.
|
||||
*/
|
||||
@NestedConfigurationProperty
|
||||
@JsonIgnore
|
||||
private Set<String> enabledFunctions = new HashSet<>();
|
||||
|
||||
/**
|
||||
* Map of bean names and their descriptions to register as function callbacks.
|
||||
* For example `spring.ai.openai.chat.options.beanFunctions.spring.ai.openai.chat.options.beanFunctions.weatherInfo` * or with
|
||||
* description `spring.ai.openai.chat.options.beanFunctions.spring.ai.openai.chat.options.beanFunctions.weatherInfo=Get the weather in location`.
|
||||
* The description is optional.
|
||||
* Each bean name should be specified in a separate property.
|
||||
*/
|
||||
@NestedConfigurationProperty
|
||||
@JsonIgnore
|
||||
private Map<String, String> beanFunctions = new HashMap<>();
|
||||
// @formatter:on
|
||||
|
||||
public static Builder builder() {
|
||||
@@ -245,6 +263,16 @@ public class OpenAiChatOptions implements ChatOptions {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withBeanFunctions(Map<String, String> beanFunctions) {
|
||||
this.options.beanFunctions = beanFunctions;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withBeanFunction(String beanName, String description) {
|
||||
this.options.beanFunctions.put(beanName, description);
|
||||
return this;
|
||||
}
|
||||
|
||||
public OpenAiChatOptions build() {
|
||||
return this.options;
|
||||
}
|
||||
@@ -383,6 +411,14 @@ public class OpenAiChatOptions implements ChatOptions {
|
||||
this.enabledFunctions = functionNames;
|
||||
}
|
||||
|
||||
public Map<String, String> getBeanFunctions() {
|
||||
return beanFunctions;
|
||||
}
|
||||
|
||||
public void setBeanFunctions(Map<String, String> beanFunctions) {
|
||||
this.beanFunctions = beanFunctions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
|
||||
@@ -21,7 +21,7 @@ import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.model.AbstractToolFunctionCallback;
|
||||
import org.springframework.ai.model.function.AbstractToolFunctionCallback;
|
||||
import org.springframework.ai.openai.api.OpenAiApi;
|
||||
import org.springframework.ai.openai.chat.api.tool.MockWeatherService;
|
||||
import org.springframework.ai.openai.chat.api.tool.MockWeatherService.Request;
|
||||
|
||||
@@ -19,7 +19,7 @@ import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.chat.prompt.PromptTemplate;
|
||||
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
|
||||
import org.springframework.ai.model.AbstractToolFunctionCallback;
|
||||
import org.springframework.ai.model.function.AbstractToolFunctionCallback;
|
||||
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||
import org.springframework.ai.openai.OpenAiTestConfiguration;
|
||||
import org.springframework.ai.openai.chat.api.tool.MockWeatherService;
|
||||
|
||||
@@ -25,6 +25,13 @@
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-function-context</artifactId>
|
||||
<version>4.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- production dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.antlr</groupId>
|
||||
|
||||
@@ -16,10 +16,7 @@
|
||||
|
||||
package org.springframework.ai.chat;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.ai.model.ModelOptions;
|
||||
import org.springframework.ai.model.ToolFunctionCallback;
|
||||
|
||||
/**
|
||||
* The ChatOptions represent the common options, portable across different chat models.
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.ai.model;
|
||||
package org.springframework.ai.model.function;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.springframework.ai.model.ModelOptionsUtils;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -112,6 +113,19 @@ public abstract class AbstractToolFunctionCallback<I, O> implements Function<I,
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public static <I, O> AbstractToolFunctionCallback<I, O> of(String name, String description,
|
||||
Function<I, O> function) {
|
||||
Assert.notNull(name, "Name must not be null");
|
||||
Assert.notNull(description, "Description must not be null");
|
||||
Assert.notNull(function, "Function must not be null");
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
final Class<I> inputClassType = (Class<I>) TypeResolverHelper
|
||||
.getFunctionInputClass((Class<Function<I, O>>) function.getClass());
|
||||
|
||||
return new DefaultToolFunctionCallback<I, O>(name, description, inputClassType, function);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.name;
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.springframework.ai.model.function;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Note that the underlying function is responsible for converting the output into format
|
||||
* that can be consumed by the Model. The default implementation converts the output into
|
||||
* String before sending it to the Model. Provide a custom function responseConverter
|
||||
* implementation to override this.
|
||||
*
|
||||
*/
|
||||
public class DefaultToolFunctionCallback<I, O> extends AbstractToolFunctionCallback<I, O> {
|
||||
|
||||
private Function<I, O> function;
|
||||
|
||||
public DefaultToolFunctionCallback(String name, String description, Class<I> inputType, Function<I, O> function) {
|
||||
super(name, description, inputType);
|
||||
Assert.notNull(function, "Function must not be null");
|
||||
this.function = function;
|
||||
}
|
||||
|
||||
public DefaultToolFunctionCallback(String name, String description, Class<I> inputType,
|
||||
Function<O, String> responseConverter, Function<I, O> function) {
|
||||
super(name, description, inputType, responseConverter);
|
||||
Assert.notNull(function, "Function must not be null");
|
||||
this.function = function;
|
||||
}
|
||||
|
||||
public DefaultToolFunctionCallback(String name, String description, Function<I, O> function) {
|
||||
this(name, description, resolveInputType(function), function);
|
||||
}
|
||||
|
||||
public DefaultToolFunctionCallback(String name, String description, Function<O, String> responseConverter,
|
||||
Function<I, O> function) {
|
||||
this(name, description, resolveInputType(function), responseConverter, function);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <I, O> Class<I> resolveInputType(Function<I, O> function) {
|
||||
return (Class<I>) TypeResolverHelper.getFunctionInputClass((Class<Function<I, O>>) function.getClass());
|
||||
}
|
||||
|
||||
@Override
|
||||
public O apply(I input) {
|
||||
return this.function.apply(input);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright 2024-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.
|
||||
*/
|
||||
package org.springframework.ai.model.function;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonClassDescription;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.cloud.function.context.catalog.FunctionTypeUtils;
|
||||
import org.springframework.cloud.function.context.config.FunctionContextUtils;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.context.annotation.Description;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* A Spring {@link ApplicationContextAware} implementation that provides a way to retrieve
|
||||
* a {@link Function} from the Spring context and wrap it into a
|
||||
* {@link ToolFunctionCallback}.
|
||||
*
|
||||
* The name of the function is determined by the bean name.
|
||||
*
|
||||
* The description of the function is determined by the following rules:
|
||||
* <ul>
|
||||
* <li>Provided as a default description</li>
|
||||
* <li>Provided as a {@code @Description} annotation on the bean</li>
|
||||
* <li>Provided as a {@code @JsonClassDescription} annotation on the input class</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @author Christopher Smith
|
||||
*/
|
||||
public class SpringAiFunctionContextManager implements ApplicationContextAware {
|
||||
|
||||
private GenericApplicationContext applicationContext;
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException {
|
||||
this.applicationContext = (GenericApplicationContext) applicationContext;
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
public ToolFunctionCallback getFunctionFromBean(@NonNull String beanName, @Nullable String defaultDescription) {
|
||||
|
||||
Type beanType = FunctionContextUtils.findType(this.applicationContext.getBeanFactory(), beanName);
|
||||
|
||||
if (beanType == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Functional bean with name: " + beanName + " does not exist in the context.");
|
||||
}
|
||||
|
||||
if (!Function.class.isAssignableFrom(FunctionTypeUtils.getRawType(beanType))) {
|
||||
throw new IllegalArgumentException(
|
||||
"Function call Bean must be of type Function. Found: " + beanType.getTypeName());
|
||||
}
|
||||
|
||||
Type functionInputType = TypeResolverHelper.getFunctionArgumentType(beanType, 0);
|
||||
|
||||
Class<?> functionInputClass = FunctionTypeUtils.getRawType(functionInputType);
|
||||
String functionName = beanName;
|
||||
String functionDescription = defaultDescription;
|
||||
|
||||
if (!StringUtils.hasText(functionDescription)) {
|
||||
// Look for a Description annotation on the bean
|
||||
Description descriptionAnnotation = applicationContext.findAnnotationOnBean(beanName, Description.class);
|
||||
|
||||
if (descriptionAnnotation != null) {
|
||||
functionDescription = descriptionAnnotation.value();
|
||||
}
|
||||
|
||||
if (!StringUtils.hasText(functionDescription)) {
|
||||
// Look for a JsonClassDescription annotation on the input class
|
||||
JsonClassDescription jsonClassDescriptionAnnotation = functionInputClass
|
||||
.getAnnotation(JsonClassDescription.class);
|
||||
if (jsonClassDescriptionAnnotation != null) {
|
||||
functionDescription = jsonClassDescriptionAnnotation.value();
|
||||
}
|
||||
}
|
||||
|
||||
if (!StringUtils.hasText(functionDescription)) {
|
||||
throw new IllegalStateException("Could not determine function description."
|
||||
+ "Please provide a description either as a default parameter, via @Description annotation on the bean "
|
||||
+ "or @JsonClassDescription annotation on the input class.");
|
||||
}
|
||||
}
|
||||
|
||||
Object bean = this.applicationContext.getBean(beanName);
|
||||
|
||||
if (bean instanceof Function<?, ?> function) {
|
||||
return new DefaultToolFunctionCallback(functionName, functionDescription, functionInputClass, function);
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException("Bean must be of type Function");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.ai.model;
|
||||
package org.springframework.ai.model.function;
|
||||
|
||||
/**
|
||||
* Represents a model function call handler. Implementations are registered with the
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2024-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.
|
||||
*/
|
||||
|
||||
package org.springframework.ai.model.function;
|
||||
|
||||
import java.lang.reflect.GenericArrayType;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.function.Function;
|
||||
|
||||
import net.jodah.typetools.TypeResolver;
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
public class TypeResolverHelper {
|
||||
|
||||
public static Class<?> getFunctionInputClass(Class<? extends Function<?, ?>> functionClass) {
|
||||
return getFunctionArgumentClass(functionClass, 0);
|
||||
}
|
||||
|
||||
public static Class<?> getFunctionOutputClass(Class<? extends Function<?, ?>> functionClass) {
|
||||
return getFunctionArgumentClass(functionClass, 1);
|
||||
}
|
||||
|
||||
public static Class<?> getFunctionArgumentClass(Class<? extends Function<?, ?>> functionClass, int argumentIndex) {
|
||||
Type type = TypeResolver.reify(Function.class, functionClass);
|
||||
|
||||
var argumentType = type instanceof ParameterizedType
|
||||
? ((ParameterizedType) type).getActualTypeArguments()[argumentIndex] : Object.class;
|
||||
|
||||
return toRawClass(argumentType);
|
||||
}
|
||||
|
||||
public static Type getFunctionInputType(Class<? extends Function<?, ?>> functionClass) {
|
||||
return getFunctionArgumentType(functionClass, 0);
|
||||
}
|
||||
|
||||
public static Type getFunctionOutputType(Class<? extends Function<?, ?>> functionClass) {
|
||||
return getFunctionArgumentType(functionClass, 1);
|
||||
}
|
||||
|
||||
public static Type getFunctionArgumentType(Class<? extends Function<?, ?>> functionClass, int argumentIndex) {
|
||||
Type functionType = TypeResolver.reify(Function.class, functionClass);
|
||||
return getFunctionArgumentType(functionType, argumentIndex);
|
||||
}
|
||||
|
||||
public static Type getFunctionArgumentType(Type functionType, int argumentIndex) {
|
||||
var argumentType = functionType instanceof ParameterizedType
|
||||
? ((ParameterizedType) functionType).getActualTypeArguments()[argumentIndex] : Object.class;
|
||||
|
||||
return argumentType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Effectively converts {@link Type} which could be {@link ParameterizedType} to raw
|
||||
* Class (no generics).
|
||||
* @param type actual {@link Type} instance
|
||||
* @return instance of {@link Class} as raw representation of the provided
|
||||
* {@link Type}
|
||||
*/
|
||||
public static Class<?> toRawClass(Type type) {
|
||||
return type != null
|
||||
? TypeResolver.resolveRawClass(type instanceof GenericArrayType ? type : TypeResolver.reify(type), null)
|
||||
: null;
|
||||
}
|
||||
|
||||
// public static void main(String[] args) {
|
||||
// Class<? extends Function<?, ?>> clazz = MockWeatherService.class;
|
||||
// System.out.println(getFunctionInputType(clazz));
|
||||
|
||||
// }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2024-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.
|
||||
*/
|
||||
|
||||
package org.springframework.ai.model.function;
|
||||
|
||||
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.model.function.TypeResolverHelperTests.MockWeatherService.Request;
|
||||
import org.springframework.ai.model.function.TypeResolverHelperTests.MockWeatherService.Response;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;;
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
public class TypeResolverHelperTests {
|
||||
|
||||
@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 MockWeatherService implements Function<Request, Response> {
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response apply(Request request) {
|
||||
return new Response(10, "C");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,15 +1,19 @@
|
||||
= Function Calling
|
||||
|
||||
You can register custom Java functions with the `OpenAiChatClient` and have the OpenAI model intelligently choose to output a JSON object containing arguments to call one or many of the registered functions.
|
||||
This is a powerful technique to connect the LLM capabilities with external tools and APIs.
|
||||
The models have been trained to detect when a function should to be called and to respond with JSON that adheres to the function signature.
|
||||
This allows you to connect the LLM capabilities with external tools and APIs.
|
||||
The OpenAI models are trained to detect when a function should to be called and to respond with JSON that adheres to the function signature.
|
||||
|
||||
Note that the OpenAI API does not call the function directly; instead, the model generates JSON that you can use to call the function in your code and return the result back to the model to complete the conversation.
|
||||
The OpenAI API does not call the function directly; instead, the model generates JSON that you can use to call the function in your code and return the result back to the model to complete the conversation.
|
||||
|
||||
Spring AI provides flexible and user-friendly ways to register and call custom functions.
|
||||
In general the custom functions need to provide a function `name`, function `description` that helps the model to understand when to call the function, and the function call `signature` (as JSON schema) to let the model know what arguments the function expects.
|
||||
|
||||
To register your custom function you need to specify a function `name`, function `description` that helps the model to understand when to call the function, and the function call `signature` (as JSON schema) to let the model know what arguments the function expects.
|
||||
Then you can implement a function that takes the function call arguments from the model interacts with the external, 3rd party, services and returns the result back to the model.
|
||||
|
||||
Spring AI offers a generic link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-core/src/main/java/org/springframework/ai/model/ToolFunctionCallback.java[ToolFunctionCallback.java] interface and the companion link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-core/src/main/java/org/springframework/ai/model/AbstractToolFunctionCallback.java[AbstractToolFunctionCallback.java] utility class to simplify the implementation and registration of Java callback functions.
|
||||
Spring AI offers a generic link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-core/src/main/java/org/springframework/ai/model/function/ToolFunctionCallback.java[ToolFunctionCallback.java] interface and the companion link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-core/src/main/java/org/springframework/ai/model/function/DefaultToolFunctionCallback.java[DefauttToolFunctionCallback.java] utility class to simplify the implementation and registration of Java callback functions.
|
||||
|
||||
Additionally the Auto-Configuration provides a way to auto-register any Function<I, O> beans definition as function calling candidates in the `ChatClient`.
|
||||
|
||||
== Quick Start
|
||||
|
||||
@@ -34,52 +38,37 @@ public class MockWeatherService implements Function<Request, Response> {
|
||||
}
|
||||
----
|
||||
|
||||
Then extend link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-core/src/main/java/org/springframework/ai/model/AbstractToolFunctionCallback.java[AbstractToolFunctionCallback] to implement our weather function like this:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
public class WeatherFunctionCallback
|
||||
extends AbstractToolFunctionCallback<Request, Response> {
|
||||
|
||||
private final MockWeatherService weatherService = new MockWeatherService();
|
||||
|
||||
public WeatherFunctionCallback(String name, String description, Class<Request> inputType) {
|
||||
super(name, // (1)
|
||||
description, // (2)
|
||||
inputType, // (3)
|
||||
(response) -> "" + response.temp() + response.unit()); // (4)
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response apply(Request request) {
|
||||
return this.weatherService.apply(request);
|
||||
}
|
||||
};
|
||||
----
|
||||
|
||||
The constructor takes a function name (1), description (2), input type signature (3) and a converter (4) to convert the `Response` into a text.
|
||||
The Spring AI auto-generates the JSON Scheme for the `MockWeatherService.Request.class` signature.
|
||||
|
||||
=== Registering Functions as Beans
|
||||
|
||||
If you enable the link:../openai-chat.html#_auto_configuration[OpenAiChatClient Auto-Configuration], the easiest way to register a function is to created it as a bean in the Spring context:
|
||||
With the link:../openai-chat.html#_auto_configuration[OpenAiChatClient Auto-Configuration] you have multiple ways to register custom functions as beans in the Spring context.
|
||||
|
||||
==== DefaultToolFunctionCallback Wrapper
|
||||
|
||||
One way to register a function is to create `DefaultToolFunctionCallback` wrapper like this:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Configuration
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
public WeatherFunctionCallback weatherFunctionInfo() {
|
||||
return new WeatherFunctionCallback(
|
||||
"CurrentWeather", // (1) function name
|
||||
"Get the weather in location", // (2) function description
|
||||
MockWeatherService.Request.class); // (3) function input signature
|
||||
public ToolFunctionCallback weatherFunctionInfo() {
|
||||
|
||||
return new DefaultToolFunctionCallback<>("CurrentWeather", // (1) function name
|
||||
"Get the weather in location", // (2) function description
|
||||
(response) -> "" + response.temp() + response.unit(), // (3) Response Converter
|
||||
new MockWeatherService()); // function code
|
||||
}
|
||||
...
|
||||
}
|
||||
----
|
||||
|
||||
Now you can enable the `CurrentWeather` function in your prompt calls:
|
||||
It wraps the 3rd party, `MockWeatherService` function and registers it as a `CurrentWeather` function with the `OpenAiChatClient`.
|
||||
It also provides a description (2) and an optional response converter (3) to convert the response into a text as expected by the model.
|
||||
|
||||
NOTE: The `DefaultToolFunctionCallback` internally resolves the function call signature based on the `MockWeatherService.Request` class.
|
||||
|
||||
To let the model know and call your `CurrentWeather` function you need to enable it in your prompt requests:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@@ -93,7 +82,7 @@ ChatResponse response = chatClient.call(new Prompt(List.of(userMessage),
|
||||
logger.info("Response: {}", response);
|
||||
----
|
||||
|
||||
NOTE: you must enable, explicitly, the functions to be used in the prompt request using the `OpenAiChatOptions.builder().withEnabledFunction(...)` method (1).
|
||||
NOTE: You can can have multiple functions registered in your `ChatClient` but only those enabled in the prompt request will be considered for the function calling.
|
||||
|
||||
Above user question will trigger 3 calls to `CurrentWeather` function (one for each city) and the final response will be something like this:
|
||||
|
||||
@@ -104,36 +93,80 @@ Here is the current weather for the requested cities:
|
||||
- Paris, France: 15.0°C
|
||||
----
|
||||
|
||||
The link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/ToolCallWithBeanFunctionRegistrationIT.java[ToolCallWithBeanFunctionRegistrationIT.java] integration test provides a complete example of how to register a function with the `OpenAiChatClient` using the auto-configuration.
|
||||
The link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/ToolCallWithDefaultToolFunctionCallbackIT.java[ToolCallWithDefaultToolFunctionCallbackIT.java] test demo this approach.
|
||||
|
||||
==== @SpringAiFunction
|
||||
|
||||
You can use the `SpringAiFunction` annotation cam be used to register a `java.util.Function<I,O>` as a `ToolFunctionCallback` bean:
|
||||
==== Plain Java Functions
|
||||
|
||||
Instead of creating a `DefaultToolFunctionCallback` wrapper you can register any plain `java.util.Function<I,O>` as a function calling candidate in the `ChatClient`:
|
||||
|
||||
You just need to list the function bean names via the `spring.ai.openai.chat.options.beanFunctions.<bean-name>` property.
|
||||
|
||||
NOTE: Each bean name should be specified in a separate property.
|
||||
|
||||
For example lets register the `CurrentWeather1` function:
|
||||
|
||||
----
|
||||
spring.ai.openai.chat.options.beanFunctions.CurrentWeather1
|
||||
----
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Configuration
|
||||
static class Config {
|
||||
|
||||
@SpringAiFunction(
|
||||
name = "CurrentWeather", // (1)
|
||||
description = "Get the weather in location", // (2)
|
||||
classType = MockWeatherService.Request.class) // (3)
|
||||
public Function<Request, Response> weatherFunction() {
|
||||
@Bean("CurrentWeather1") // (1) use the bean alias as function name.
|
||||
@Description("Get the weather in location") // (2) function description
|
||||
public Function<MockWeatherService.Request, MockWeatherService.Response> weatherFunction1() {
|
||||
MockWeatherService weatherService = new MockWeatherService();
|
||||
return (weatherService::apply);
|
||||
}
|
||||
|
||||
...
|
||||
}
|
||||
----
|
||||
|
||||
The `@SpringAiFunction` annotation defines the function name (1), description (2), and input signature (3) and registers the function as a bean in the Spring context.
|
||||
The `@Description` annotation is optional and provides a function description (2) that helps the model to understand when to call the function.
|
||||
|
||||
NOTE: The `SpringAiFunction` annotation supported only if the auto-configuration is enabled.
|
||||
Instead of using the `@Description` annotation you can also provide the function description via the `spring.ai.openai.chat.options.beanFunctions.<bean-name>=<description>` property:
|
||||
|
||||
NOTE: The Function<I, O> implementation is responsible to convert the response into a text as expected by the model.
|
||||
By default, the `AbstractToolFunctionCallback` provides a default converter that returns the `toString()` of the response object.
|
||||
----
|
||||
spring.ai.openai.chat.options.beanFunctions.currentWeather2=Get the weather in location
|
||||
----
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Configuration
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
public Function<MockWeatherService.Request, MockWeatherService.Response> currentWeather2() { // (1) bean name as function name.
|
||||
MockWeatherService weatherService = new MockWeatherService();
|
||||
return (weatherService::apply);
|
||||
}
|
||||
...
|
||||
}
|
||||
----
|
||||
|
||||
Another options is to use the `JacksonDescription` annotation on the `MockWeatherService.Request` to provide the function description:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
|
||||
@Configuration
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
public Function<Request, Response> currentWeather3() { // (1) bean name as function name.
|
||||
MockWeatherService weatherService = new MockWeatherService();
|
||||
return (weatherService::apply);
|
||||
}
|
||||
...
|
||||
}
|
||||
|
||||
@JsonClassDescription("Get the weather in location") // (2) function description
|
||||
public record Request(String location, Unit unit) {}
|
||||
|
||||
----
|
||||
|
||||
=== Register/Call Functions with Prompt Options
|
||||
|
||||
@@ -146,10 +179,10 @@ OpenAiChatClient chatClient = ...
|
||||
UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?");
|
||||
|
||||
var promptOptions = OpenAiChatOptions.builder()
|
||||
.withToolCallbacks(List.of(new WeatherFunctionCallback(
|
||||
"CurrentWeather",
|
||||
"Get the weather in location",
|
||||
MockWeatherService.Request.class)))
|
||||
.withToolCallbacks(List.of(new DefaultToolFunctionCallback<>(
|
||||
"CurrentWeather", // name
|
||||
"Get the weather in location", // function description
|
||||
new MockWeatherService()))) // function code
|
||||
.build();
|
||||
|
||||
ChatResponse response = chatClient.call(new Prompt(List.of(userMessage), promptOptions));
|
||||
|
||||
@@ -73,6 +73,8 @@ The prefix `spring.ai.openai.chat` is the property prefix that lets you configur
|
||||
| spring.ai.openai.chat.options.tools | A list of tools the model may call. Currently, only functions are supported as a tool. Use this to provide a list of functions the model may generate JSON inputs for. | -
|
||||
| spring.ai.openai.chat.options.toolChoice | Controls which (if any) function is called by the model. none means the model will not call a function and instead generates a message. auto means the model can pick between generating a message or calling a function. Specifying a particular function via {"type: "function", "function": {"name": "my_function"}} forces the model to call that function. none is the default when no functions are present. auto is the default if functions are present. | -
|
||||
| spring.ai.openai.chat.options.user | A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. | -
|
||||
| spring.ai.openai.chat.options.enabledFunctions | List of functions, identified by their names, to enable for function calling in a single prompt requests. Functions with those names must exist in the toolCallbacks registry. | -
|
||||
| spring.ai.openai.chat.options.beanFunctions.<function-name>.<description> | Map of bean names and their descriptions to register as function callbacks. For example `s.a.o.c.options.beanFunctions.weatherInfo` or with description `s.a.o.c.options.beanFunctions.weatherInfo=Get the weather in location`. The description is optional. Each bean name should be specified in a separate property. | -
|
||||
|====
|
||||
|
||||
NOTE: You can override the common `spring.ai.openai.base-url` and `spring.ai.openai.api-key` for the `ChatClient` and `EmbeddingClient` implementations.
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024-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.
|
||||
*/
|
||||
|
||||
package org.springframework.ai.autoconfigure.common.function;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
/**
|
||||
* An annotation used to define functions for use in
|
||||
*
|
||||
* @author Christopher Smith
|
||||
*/
|
||||
@Bean
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface SpringAiFunction {
|
||||
|
||||
String name();
|
||||
|
||||
String description();
|
||||
|
||||
Class<?> classType();
|
||||
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024-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.
|
||||
*/
|
||||
package org.springframework.ai.autoconfigure.common.function;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.ai.model.AbstractToolFunctionCallback;
|
||||
import org.springframework.ai.model.ToolFunctionCallback;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Manages the chat functions that are annotated with {@link SpringAiFunction}.
|
||||
*
|
||||
* @author Christopher Smith
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
public class SpringAiFunctionAnnotationManager implements ApplicationContextAware {
|
||||
|
||||
private GenericApplicationContext applicationContext;
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException {
|
||||
this.applicationContext = (GenericApplicationContext) applicationContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a list of all the java.util.Functions annotated with
|
||||
* {@link SpringAiFunction}.
|
||||
*/
|
||||
public List<ToolFunctionCallback> getAnnotatedToolFunctionCallbacks() {
|
||||
Map<String, Object> beans = this.applicationContext.getBeansWithAnnotation(SpringAiFunction.class);
|
||||
|
||||
List<ToolFunctionCallback> toolFunctionCallbacks = new ArrayList<>();
|
||||
|
||||
if (!CollectionUtils.isEmpty(beans)) {
|
||||
|
||||
beans.forEach((k, v) -> {
|
||||
if (v instanceof Function<?, ?> function) {
|
||||
SpringAiFunction functionAnnotation = applicationContext.findAnnotationOnBean(k,
|
||||
SpringAiFunction.class);
|
||||
|
||||
toolFunctionCallbacks.add(new SpringAiFunctionToolFunctionCallback(functionAnnotation.name(),
|
||||
functionAnnotation.description(), functionAnnotation.classType(), function));
|
||||
}
|
||||
else {
|
||||
ReflectionUtils.handleReflectionException(new IllegalArgumentException(
|
||||
"Bean annotated with @SpringAiFunction must be of type Function"));
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
return toolFunctionCallbacks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note that the underlying function is responsible for converting the output into
|
||||
* format that can be consumed by the Model. The default implementation converts the
|
||||
* output into String before sending it to the Model. Provide a custom function
|
||||
* responseConverter implementation to override this.
|
||||
*
|
||||
*/
|
||||
public static class SpringAiFunctionToolFunctionCallback<I, O> extends AbstractToolFunctionCallback<I, O> {
|
||||
|
||||
private Function<I, O> function;
|
||||
|
||||
protected SpringAiFunctionToolFunctionCallback(String name, String description, Class<I> inputType,
|
||||
Function<I, O> function) {
|
||||
super(name, description, inputType);
|
||||
Assert.notNull(function, "Function must not be null");
|
||||
this.function = function;
|
||||
}
|
||||
|
||||
protected SpringAiFunctionToolFunctionCallback(String name, String description, Class<I> inputType,
|
||||
Function<O, String> responseConverter, Function<I, O> function) {
|
||||
super(name, description, inputType, responseConverter);
|
||||
Assert.notNull(function, "Function must not be null");
|
||||
this.function = function;
|
||||
}
|
||||
|
||||
@Override
|
||||
public O apply(I input) {
|
||||
return this.function.apply(input);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,9 +19,9 @@ package org.springframework.ai.autoconfigure.openai;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.ai.autoconfigure.NativeHints;
|
||||
import org.springframework.ai.autoconfigure.common.function.SpringAiFunctionAnnotationManager;
|
||||
import org.springframework.ai.embedding.EmbeddingClient;
|
||||
import org.springframework.ai.model.ToolFunctionCallback;
|
||||
import org.springframework.ai.model.function.SpringAiFunctionContextManager;
|
||||
import org.springframework.ai.model.function.ToolFunctionCallback;
|
||||
import org.springframework.ai.openai.OpenAiChatClient;
|
||||
import org.springframework.ai.openai.OpenAiEmbeddingClient;
|
||||
import org.springframework.ai.openai.OpenAiImageClient;
|
||||
@@ -58,7 +58,7 @@ public class OpenAiAutoConfiguration {
|
||||
@ConditionalOnMissingBean
|
||||
public OpenAiChatClient openAiChatClient(OpenAiConnectionProperties commonProperties,
|
||||
OpenAiChatProperties chatProperties, RestClient.Builder restClientBuilder,
|
||||
List<ToolFunctionCallback> toolFunctionCallbacks, SpringAiFunctionAnnotationManager functionManager) {
|
||||
List<ToolFunctionCallback> toolFunctionCallbacks, SpringAiFunctionContextManager functionManager) {
|
||||
|
||||
String apiKey = StringUtils.hasText(chatProperties.getApiKey()) ? chatProperties.getApiKey()
|
||||
: commonProperties.getApiKey();
|
||||
@@ -75,9 +75,11 @@ public class OpenAiAutoConfiguration {
|
||||
chatProperties.getOptions().getToolCallbacks().addAll(toolFunctionCallbacks);
|
||||
}
|
||||
|
||||
var annotatedFunctionsList = functionManager.getAnnotatedToolFunctionCallbacks();
|
||||
if (!CollectionUtils.isEmpty(annotatedFunctionsList)) {
|
||||
chatProperties.getOptions().getToolCallbacks().addAll(annotatedFunctionsList);
|
||||
if (!CollectionUtils.isEmpty(chatProperties.getOptions().getBeanFunctions())) {
|
||||
chatProperties.getOptions().getBeanFunctions().forEach((beanName, description) -> {
|
||||
ToolFunctionCallback function = functionManager.getFunctionFromBean(beanName, description);
|
||||
chatProperties.getOptions().getToolCallbacks().add(function);
|
||||
});
|
||||
}
|
||||
|
||||
return new OpenAiChatClient(openAiApi, chatProperties.getOptions());
|
||||
@@ -122,8 +124,8 @@ public class OpenAiAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public SpringAiFunctionAnnotationManager springAiFunctionManager(ApplicationContext context) {
|
||||
SpringAiFunctionAnnotationManager manager = new SpringAiFunctionAnnotationManager();
|
||||
public SpringAiFunctionContextManager springAiFunctionManager(ApplicationContext context) {
|
||||
SpringAiFunctionContextManager manager = new SpringAiFunctionContextManager();
|
||||
manager.setApplicationContext(context);
|
||||
return manager;
|
||||
}
|
||||
|
||||
@@ -17,31 +17,32 @@
|
||||
package org.springframework.ai.autoconfigure.openai.tool;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
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.autoconfigure.common.function.SpringAiFunction;
|
||||
import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.model.function.DefaultToolFunctionCallback;
|
||||
import org.springframework.ai.model.function.ToolFunctionCallback;
|
||||
import org.springframework.ai.openai.OpenAiChatClient;
|
||||
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*")
|
||||
class ToolCallWithSpringAIFunctionAnnotationIT {
|
||||
public class TollCallWithDefaultToolFunctionCallbackIT {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ToolCallWithBeanFunctionRegistrationIT.class);
|
||||
private final Logger logger = LoggerFactory.getLogger(TollCallWithDefaultToolFunctionCallbackIT.class);
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY"))
|
||||
@@ -61,7 +62,7 @@ class ToolCallWithSpringAIFunctionAnnotationIT {
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
|
||||
assertThat(response.getResult().getOutput().getContent()).contains("30", "10", "15");
|
||||
assertThat(response.getResult().getOutput().getContent()).contains("30.0", "10.0", "15.0");
|
||||
|
||||
});
|
||||
}
|
||||
@@ -69,11 +70,13 @@ class ToolCallWithSpringAIFunctionAnnotationIT {
|
||||
@Configuration
|
||||
static class Config {
|
||||
|
||||
@SpringAiFunction(name = "WeatherInfo", description = "Get the weather in location",
|
||||
classType = MockWeatherService.Request.class)
|
||||
public Function<MockWeatherService.Request, MockWeatherService.Response> weatherFunction() {
|
||||
MockWeatherService weatherService = new MockWeatherService();
|
||||
return (weatherService::apply);
|
||||
@Bean
|
||||
public ToolFunctionCallback weatherFunctionInfo() {
|
||||
|
||||
return new DefaultToolFunctionCallback<>("WeatherInfo", // function name
|
||||
"Get the weather in location", // function description
|
||||
(response) -> "" + response.temp() + response.unit(), // responseConverter
|
||||
new MockWeatherService()); // function code
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024-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.
|
||||
*/
|
||||
|
||||
package org.springframework.ai.autoconfigure.openai.tool;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
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.autoconfigure.openai.OpenAiAutoConfiguration;
|
||||
import org.springframework.ai.autoconfigure.openai.tool.MockWeatherService.Request;
|
||||
import org.springframework.ai.autoconfigure.openai.tool.MockWeatherService.Response;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.model.AbstractToolFunctionCallback;
|
||||
import org.springframework.ai.openai.OpenAiChatClient;
|
||||
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*")
|
||||
public class ToolCallWithBeanFunctionRegistrationIT {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ToolCallWithBeanFunctionRegistrationIT.class);
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY"))
|
||||
.withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class, OpenAiAutoConfiguration.class))
|
||||
.withUserConfiguration(Config.class);
|
||||
|
||||
@Test
|
||||
void functionCallTest() {
|
||||
contextRunner.withPropertyValues("spring.ai.openai.chat.options.model=gpt-4-1106-preview").run(context -> {
|
||||
|
||||
OpenAiChatClient chatClient = context.getBean(OpenAiChatClient.class);
|
||||
|
||||
UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?");
|
||||
|
||||
ChatResponse response = chatClient.call(new Prompt(List.of(userMessage),
|
||||
OpenAiChatOptions.builder().withEnabledFunction("WeatherInfo").build()));
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
|
||||
assertThat(response.getResult().getOutput().getContent()).contains("30.0", "10.0", "15.0");
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
public WeatherFunctionCallback weatherFunctionInfo() {
|
||||
return new WeatherFunctionCallback("WeatherInfo", "Get the weather in location",
|
||||
MockWeatherService.Request.class);
|
||||
}
|
||||
|
||||
public static class WeatherFunctionCallback
|
||||
extends AbstractToolFunctionCallback<MockWeatherService.Request, MockWeatherService.Response> {
|
||||
|
||||
public WeatherFunctionCallback(String name, String description, Class<Request> inputType) {
|
||||
super(name, description, inputType, (response) -> "" + response.temp() + response.unit());
|
||||
}
|
||||
|
||||
private final MockWeatherService weatherService = new MockWeatherService();
|
||||
|
||||
@Override
|
||||
public Response apply(Request request) {
|
||||
return weatherService.apply(request);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright 2024-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.
|
||||
*/
|
||||
|
||||
package org.springframework.ai.autoconfigure.openai.tool;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
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.autoconfigure.openai.OpenAiAutoConfiguration;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.openai.OpenAiChatClient;
|
||||
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Description;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*")
|
||||
class ToolCallWithPlainBeanRegistrationIT {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ToolCallWithPlainBeanRegistrationIT.class);
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY"))
|
||||
.withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class, OpenAiAutoConfiguration.class))
|
||||
.withUserConfiguration(Config.class);
|
||||
|
||||
@Test
|
||||
void functionCallTest() {
|
||||
contextRunner.withPropertyValues("spring.ai.openai.chat.options.model=gpt-4-1106-preview",
|
||||
// ).run(context -> {
|
||||
"spring.ai.openai.chat.options.beanFunctions.weatherFunction",
|
||||
"spring.ai.openai.chat.options.beanFunctions.weatherFunction2=Get the weather in location",
|
||||
"spring.ai.openai.chat.options.beanFunctions.weatherFunction3")
|
||||
.run(context -> {
|
||||
|
||||
OpenAiChatClient chatClient = context.getBean(OpenAiChatClient.class);
|
||||
|
||||
UserMessage userMessage = new UserMessage(
|
||||
"What's the weather like in San Francisco, Tokyo, and Paris?");
|
||||
|
||||
ChatResponse response = chatClient.call(new Prompt(List.of(userMessage),
|
||||
OpenAiChatOptions.builder().withEnabledFunction("weatherFunction").build()));
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
|
||||
assertThat(response.getResult().getOutput().getContent()).contains("30", "10", "15");
|
||||
|
||||
response = chatClient.call(new Prompt(List.of(userMessage),
|
||||
OpenAiChatOptions.builder().withEnabledFunction("weatherFunction2").build()));
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
|
||||
assertThat(response.getResult().getOutput().getContent()).contains("30", "10", "15");
|
||||
|
||||
response = chatClient.call(new Prompt(List.of(userMessage),
|
||||
OpenAiChatOptions.builder().withEnabledFunction("weatherFunction3").build()));
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
|
||||
assertThat(response.getResult().getOutput().getContent()).contains("30", "10", "15");
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
@Description("Get the weather in location")
|
||||
public Function<MockWeatherService.Request, MockWeatherService.Response> weatherFunction() {
|
||||
MockWeatherService weatherService = new MockWeatherService();
|
||||
return (weatherService::apply);
|
||||
}
|
||||
|
||||
@Bean(name = "weatherFunction2")
|
||||
public Function<MockWeatherService.Request, MockWeatherService.Response> weatherFunction1() {
|
||||
MockWeatherService weatherService = new MockWeatherService();
|
||||
return (weatherService::apply);
|
||||
}
|
||||
|
||||
// Relies on the Request's JsonClassDescription annotation to provide the
|
||||
// function description.
|
||||
@Bean
|
||||
public Function<MockWeatherService.Request, MockWeatherService.Response> weatherFunction3() {
|
||||
MockWeatherService weatherService = new MockWeatherService();
|
||||
return (weatherService::apply);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -27,7 +27,7 @@ import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.model.AbstractToolFunctionCallback;
|
||||
import org.springframework.ai.model.function.DefaultToolFunctionCallback;
|
||||
import org.springframework.ai.openai.OpenAiChatClient;
|
||||
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
@@ -54,18 +54,10 @@ public class ToolCallWithPromptFunctionRegistrationIT {
|
||||
UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?");
|
||||
|
||||
var promptOptions = OpenAiChatOptions.builder()
|
||||
.withToolCallbacks(List
|
||||
.of(new AbstractToolFunctionCallback<MockWeatherService.Request, MockWeatherService.Response>(
|
||||
"CurrentWeatherService", "Get the weather in location", MockWeatherService.Request.class,
|
||||
(response) -> "" + response.temp() + response.unit()) {
|
||||
|
||||
private final MockWeatherService weatherService = new MockWeatherService();
|
||||
|
||||
@Override
|
||||
public MockWeatherService.Response apply(MockWeatherService.Request request) {
|
||||
return weatherService.apply(request);
|
||||
}
|
||||
}))
|
||||
.withToolCallbacks(List.of(new DefaultToolFunctionCallback<>("CurrentWeatherService", // name
|
||||
"Get the weather in location", // function description
|
||||
(response) -> "" + response.temp() + response.unit(), // responseConverter
|
||||
new MockWeatherService()))) // function code
|
||||
.build();
|
||||
|
||||
ChatResponse response = chatClient.call(new Prompt(List.of(userMessage), promptOptions));
|
||||
|
||||
Reference in New Issue
Block a user