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:
Christian Tzolov
2024-02-15 12:39:21 +01:00
parent 43dbaf4edd
commit 1fa2036120
21 changed files with 641 additions and 347 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>

View File

@@ -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.

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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");
}
}
}

View File

@@ -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

View File

@@ -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));
// }
}

View File

@@ -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");
}
}
}

View File

@@ -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));

View File

@@ -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.

View File

@@ -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();
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}

View File

@@ -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
}
}

View File

@@ -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);
}
};
}
}

View File

@@ -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);
}
}
}

View File

@@ -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));