Simplify Function Calling API

This commit is contained in:
Christian Tzolov
2024-02-15 20:26:31 +01:00
parent 10438c55a3
commit d580fe2757
20 changed files with 237 additions and 272 deletions

View File

@@ -37,7 +37,8 @@ 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.function.ToolFunctionCallback;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallbackContext;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletion;
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletionMessage;
@@ -72,10 +73,26 @@ public class OpenAiChatClient implements ChatClient, StreamingChatClient {
private final Logger logger = LoggerFactory.getLogger(getClass());
/**
* The default options used for the chat completion requests.
*/
private OpenAiChatOptions defaultOptions;
private Map<String, ToolFunctionCallback> toolCallbackRegister = new ConcurrentHashMap<>();
/**
* The function callback register is used to resolve the function callbacks by name.
*/
private Map<String, FunctionCallback> functionCallbackRegister = new ConcurrentHashMap<>();
/**
* The function callback context is used to resolve the function callbacks by name
* from the Spring context. It is optional and usually used with Spring
* auto-configuration.
*/
private FunctionCallbackContext functionCallbackContext;
/**
* The retry template used to retry the OpenAI API calls.
*/
public final RetryTemplate retryTemplate = RetryTemplate.builder()
.maxAttempts(10)
.retryOn(OpenAiApiException.class)
@@ -89,17 +106,35 @@ public class OpenAiChatClient implements ChatClient, StreamingChatClient {
})
.build();
/**
* Low-level access to the OpenAI API.
*/
private final OpenAiApi openAiApi;
public OpenAiChatClient(OpenAiApi openAiApi) {
this(openAiApi, OpenAiChatOptions.builder().withModel("gpt-3.5-turbo").withTemperature(0.7f).build());
this(openAiApi,
OpenAiChatOptions.builder().withModel(OpenAiApi.DEFAULT_CHAT_MODEL).withTemperature(0.7f).build());
}
public OpenAiChatClient(OpenAiApi openAiApi, OpenAiChatOptions options) {
this(openAiApi, options, null);
}
public OpenAiChatClient(OpenAiApi openAiApi, OpenAiChatOptions options,
FunctionCallbackContext functionCallbackContext) {
Assert.notNull(openAiApi, "OpenAiApi must not be null");
Assert.notNull(options, "Options must not be null");
this.openAiApi = openAiApi;
this.defaultOptions = options;
this.functionCallbackContext = functionCallbackContext;
// Register the default function callbacks.
if (!CollectionUtils.isEmpty(this.defaultOptions.getFunctionCallbacks())) {
this.defaultOptions.getFunctionCallbacks()
.stream()
.forEach(functionCallback -> this.functionCallbackRegister.put(functionCallback.getName(),
functionCallback));
}
}
/**
@@ -188,7 +223,7 @@ public class OpenAiChatClient implements ChatClient, StreamingChatClient {
OpenAiChatOptions updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(runtimeOptions,
ChatOptions.class, OpenAiChatOptions.class);
Set<String> promptEnabledFunctions = handleToolFunctionConfigurations(updatedRuntimeOptions, true,
Set<String> promptEnabledFunctions = handleFunctionCallbackConfigurations(updatedRuntimeOptions, true,
true);
enabledFunctionsForRequest.addAll(promptEnabledFunctions);
@@ -202,7 +237,8 @@ public class OpenAiChatClient implements ChatClient, StreamingChatClient {
if (this.defaultOptions != null) {
Set<String> defaultEnabledFunctions = handleToolFunctionConfigurations(this.defaultOptions, false, false);
Set<String> defaultEnabledFunctions = handleFunctionCallbackConfigurations(this.defaultOptions, false,
false);
enabledFunctionsForRequest.addAll(defaultEnabledFunctions);
@@ -224,26 +260,26 @@ public class OpenAiChatClient implements ChatClient, StreamingChatClient {
return request;
}
private Set<String> handleToolFunctionConfigurations(OpenAiChatOptions options, boolean autoEnableCallbackFunctions,
boolean overrideCallbackFunctionsRegister) {
private Set<String> handleFunctionCallbackConfigurations(OpenAiChatOptions options,
boolean autoEnableCallbackFunctions, boolean overrideCallbackFunctionsRegister) {
Set<String> enabledFunctions = new HashSet<>();
if (options != null) {
if (!CollectionUtils.isEmpty(options.getToolCallbacks())) {
options.getToolCallbacks().stream().forEach(toolCallback -> {
if (!CollectionUtils.isEmpty(options.getFunctionCallbacks())) {
options.getFunctionCallbacks().stream().forEach(functionCallback -> {
// Register the tool callback.
if (overrideCallbackFunctionsRegister) {
this.toolCallbackRegister.put(toolCallback.getName(), toolCallback);
this.functionCallbackRegister.put(functionCallback.getName(), functionCallback);
}
else {
this.toolCallbackRegister.putIfAbsent(toolCallback.getName(), toolCallback);
this.functionCallbackRegister.putIfAbsent(functionCallback.getName(), functionCallback);
}
// Automatically enable the function, usually from prompt callback.
if (autoEnableCallbackFunctions) {
enabledFunctions.add(toolCallback.getName());
enabledFunctions.add(functionCallback.getName());
}
});
}
@@ -260,18 +296,32 @@ public class OpenAiChatClient implements ChatClient, StreamingChatClient {
/**
* @return returns the registered tool callbacks.
*/
Map<String, ToolFunctionCallback> getToolCallbackRegister() {
return toolCallbackRegister;
Map<String, FunctionCallback> getFunctionCallbackRegister() {
return functionCallbackRegister;
}
public List<OpenAiApi.FunctionTool> getFunctionTools(Set<String> functionNames) {
private List<OpenAiApi.FunctionTool> getFunctionTools(Set<String> functionNames) {
List<OpenAiApi.FunctionTool> functionTools = new ArrayList<>();
for (String functionName : functionNames) {
if (!this.toolCallbackRegister.containsKey(functionName)) {
throw new IllegalStateException("No function callback found for function name: " + functionName);
if (!this.functionCallbackRegister.containsKey(functionName)) {
if (this.functionCallbackContext != null) {
FunctionCallback functionCallback = this.functionCallbackContext.getFunctionCallback(functionName,
null);
if (functionCallback != null) {
this.functionCallbackRegister.put(functionName, functionCallback);
}
else {
throw new IllegalStateException(
"No function callback [" + functionName + "] fund in tht FunctionCallbackContext");
}
}
else {
throw new IllegalStateException("No function callback found for name: " + functionName);
}
}
ToolFunctionCallback functionCallback = this.toolCallbackRegister.get(functionName);
FunctionCallback functionCallback = this.functionCallbackRegister.get(functionName);
var function = new OpenAiApi.FunctionTool.Function(functionCallback.getDescription(),
functionCallback.getName(), functionCallback.getInputTypeSchema());
@@ -320,11 +370,11 @@ public class OpenAiChatClient implements ChatClient, StreamingChatClient {
var functionName = toolCall.function().name();
String functionArguments = toolCall.function().arguments();
if (!this.toolCallbackRegister.containsKey(functionName)) {
if (!this.functionCallbackRegister.containsKey(functionName)) {
throw new IllegalStateException("No function callback found for function name: " + functionName);
}
String functionResponse = this.toolCallbackRegister.get(functionName).call(functionArguments);
String functionResponse = this.functionCallbackRegister.get(functionName).call(functionArguments);
// Add the function response to the conversation.
conversationMessages.add(new ChatCompletionMessage(functionResponse, Role.TOOL, null, toolCall.id(), null));

View File

@@ -17,7 +17,6 @@
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;
@@ -29,12 +28,12 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.ai.chat.ChatOptions;
import org.springframework.ai.model.function.ToolFunctionCallback;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletionRequest.ResponseFormat;
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletionRequest.ToolChoice;
import org.springframework.ai.openai.api.OpenAiApi.FunctionTool;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.util.Assert;
import org.springframework.ai.openai.api.OpenAiApi.FunctionTool;
/**
* @author Christian Tzolov
@@ -127,19 +126,19 @@ public class OpenAiChatOptions implements ChatOptions {
/**
* OpenAI Tool Function Callbacks to register with the ChatClient.
* For Prompt Options the toolCallbacks are automatically enabled for the duration of the prompt execution.
* For Default Options the toolCallbacks are registered but disabled by default. Use the enableFunctions to set the functions
* For Prompt Options the functionCallbacks are automatically enabled for the duration of the prompt execution.
* For Default Options the functionCallbacks 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<>();
private List<FunctionCallback> functionCallbacks = new ArrayList<>();
/**
* List of functions, identified by their names, to configure for function calling in
* the chat completion requests.
* Functions with those names must exist in the toolCallbacks registry.
* The {@link #toolCallbacks} from the PromptOptions are automatically enabled for the duration of the prompt execution.
* Functions with those names must exist in the functionCallbacks registry.
* The {@link #functionCallbacks} from the PromptOptions are automatically enabled for the duration of the prompt execution.
*
* 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.
@@ -147,17 +146,6 @@ public class OpenAiChatOptions implements ChatOptions {
@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() {
@@ -246,8 +234,8 @@ public class OpenAiChatOptions implements ChatOptions {
return this;
}
public Builder withToolCallbacks(List<ToolFunctionCallback> toolCallbacks) {
this.options.toolCallbacks = toolCallbacks;
public Builder withFunctionCallbacks(List<FunctionCallback> functionCallbacks) {
this.options.functionCallbacks = functionCallbacks;
return this;
}
@@ -263,16 +251,6 @@ 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;
}
@@ -395,12 +373,12 @@ public class OpenAiChatOptions implements ChatOptions {
this.user = user;
}
public List<ToolFunctionCallback> getToolCallbacks() {
return this.toolCallbacks;
public List<FunctionCallback> getFunctionCallbacks() {
return this.functionCallbacks;
}
public void setToolCallbacks(List<ToolFunctionCallback> toolCallbacks) {
this.toolCallbacks = toolCallbacks;
public void setFunctionCallbacks(List<FunctionCallback> functionCallbacks) {
this.functionCallbacks = functionCallbacks;
}
public Set<String> getEnabledFunctions() {
@@ -411,14 +389,6 @@ 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

@@ -50,7 +50,7 @@ public class OpenAiEmbeddingClient extends AbstractEmbeddingClient {
private static final Logger logger = LoggerFactory.getLogger(OpenAiEmbeddingClient.class);
public static final String DEFAULT_OPENAI_EMBEDDING_MODEL = "text-embedding-ada-002";
public static final String DEFAULT_OPENAI_EMBEDDING_MODEL = "text-embedding-3-large";
private final OpenAiEmbeddingOptions defaultOptions;

View File

@@ -53,7 +53,8 @@ import org.springframework.web.reactive.function.client.WebClient;
public class OpenAiApi {
private static final String DEFAULT_BASE_URL = "https://api.openai.com";
private static final String DEFAULT_EMBEDDING_MODEL = "text-embedding-ada-002";
public static final String DEFAULT_CHAT_MODEL = "gpt-3.5-turbo";
public static final String DEFAULT_EMBEDDING_MODEL = "text-embedding-ada-002";
private static final Predicate<String> SSE_DONE_PREDICATE = "[DONE]"::equals;
private final RestClient restClient;

View File

@@ -21,11 +21,9 @@ import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.function.AbstractToolFunctionCallback;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
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;
import org.springframework.ai.openai.chat.api.tool.MockWeatherService.Response;
import static org.assertj.core.api.Assertions.assertThat;
@@ -63,23 +61,21 @@ public class ChatCompletionRequestTests {
final String TOOL_FUNCTION_NAME = "CurrentWeather";
var client = new OpenAiChatClient(new OpenAiApi("TEST"))
.withDefaultOptions(OpenAiChatOptions.builder().withModel("DEFAULT_MODEL").build());
var client = new OpenAiChatClient(new OpenAiApi("TEST"),
OpenAiChatOptions.builder().withModel("DEFAULT_MODEL").build());
var request = client.createRequest(new Prompt("Test message content", OpenAiChatOptions.builder()
.withModel("PROMPT_MODEL")
.withToolCallbacks(
List.of(new AbstractToolFunctionCallback<MockWeatherService.Request, MockWeatherService.Response>(
TOOL_FUNCTION_NAME, "Get the weather in location", MockWeatherService.Request.class) {
@Override
public Response apply(Request request) {
return new MockWeatherService().apply(request);
}
}))
.build()), false);
var request = client.createRequest(
new Prompt("Test message content",
OpenAiChatOptions.builder()
.withModel("PROMPT_MODEL")
.withFunctionCallbacks(List.of(new FunctionCallbackWrapper<>(TOOL_FUNCTION_NAME,
"Get the weather in location", (response) -> "" + response.temp() + response.unit(),
new MockWeatherService())))
.build()),
false);
assertThat(client.getToolCallbackRegister()).hasSize(1);
assertThat(client.getToolCallbackRegister()).containsKeys(TOOL_FUNCTION_NAME);
assertThat(client.getFunctionCallbackRegister()).hasSize(1);
assertThat(client.getFunctionCallbackRegister()).containsKeys(TOOL_FUNCTION_NAME);
assertThat(request.messages()).hasSize(1);
assertThat(request.stream()).isFalse();
@@ -94,23 +90,19 @@ public class ChatCompletionRequestTests {
final String TOOL_FUNCTION_NAME = "CurrentWeather";
var client = new OpenAiChatClient(new OpenAiApi("TEST")).withDefaultOptions(OpenAiChatOptions.builder()
.withModel("DEFAULT_MODEL")
.withToolCallbacks(
List.of(new AbstractToolFunctionCallback<MockWeatherService.Request, MockWeatherService.Response>(
TOOL_FUNCTION_NAME, "Get the weather in location", MockWeatherService.Request.class) {
@Override
public Response apply(Request request) {
return new MockWeatherService().apply(request);
}
}))
.build());
var client = new OpenAiChatClient(new OpenAiApi("TEST"),
OpenAiChatOptions.builder()
.withModel("DEFAULT_MODEL")
.withFunctionCallbacks(
List.of(new FunctionCallbackWrapper<>(TOOL_FUNCTION_NAME, "Get the weather in location",
(response) -> "" + response.temp() + response.unit(), new MockWeatherService())))
.build());
var request = client.createRequest(new Prompt("Test message content"), false);
assertThat(client.getToolCallbackRegister()).hasSize(1);
assertThat(client.getToolCallbackRegister()).containsKeys(TOOL_FUNCTION_NAME);
assertThat(client.getToolCallbackRegister().get(TOOL_FUNCTION_NAME).getDescription())
assertThat(client.getFunctionCallbackRegister()).hasSize(1);
assertThat(client.getFunctionCallbackRegister()).containsKeys(TOOL_FUNCTION_NAME);
assertThat(client.getFunctionCallbackRegister().get(TOOL_FUNCTION_NAME).getDescription())
.isEqualTo("Get the weather in location");
assertThat(request.messages()).hasSize(1);
@@ -129,27 +121,20 @@ public class ChatCompletionRequestTests {
.isEqualTo(TOOL_FUNCTION_NAME);
// Override the default options function with one from the prompt
request = client
.createRequest(new Prompt("Test message content",
OpenAiChatOptions.builder()
.withToolCallbacks(List
.of(new AbstractToolFunctionCallback<MockWeatherService.Request, String>(TOOL_FUNCTION_NAME,
"Overridden function description", MockWeatherService.Request.class) {
@Override
public String apply(Request request) {
return "Mock response";
}
}))
.build()),
false);
request = client.createRequest(new Prompt("Test message content",
OpenAiChatOptions.builder()
.withFunctionCallbacks(List.of(new FunctionCallbackWrapper<>(TOOL_FUNCTION_NAME,
"Overridden function description", new MockWeatherService())))
.build()),
false);
assertThat(request.tools()).hasSize(1);
assertThat(request.tools().get(0).function().name()).as("Explicitly enabled function")
.isEqualTo(TOOL_FUNCTION_NAME);
assertThat(client.getToolCallbackRegister()).hasSize(1);
assertThat(client.getToolCallbackRegister()).containsKeys(TOOL_FUNCTION_NAME);
assertThat(client.getToolCallbackRegister().get(TOOL_FUNCTION_NAME).getDescription())
assertThat(client.getFunctionCallbackRegister()).hasSize(1);
assertThat(client.getFunctionCallbackRegister()).containsKeys(TOOL_FUNCTION_NAME);
assertThat(client.getFunctionCallbackRegister().get(TOOL_FUNCTION_NAME).getDescription())
.isEqualTo("Overridden function description");
}

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.function.AbstractToolFunctionCallback;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.OpenAiTestConfiguration;
import org.springframework.ai.openai.chat.api.tool.MockWeatherService;
@@ -172,20 +172,10 @@ class OpenAiChatClientIT extends AbstractIT {
List<Message> messages = new ArrayList<>(List.of(userMessage));
var promptOptions = OpenAiChatOptions.builder()
.withModel("gpt-4-1106-preview")
.withToolCallbacks(
List.of(new AbstractToolFunctionCallback<MockWeatherService.Request, MockWeatherService.Response>(
"getCurrentWeather", "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);
}
}))
.withModel("gpt-4-turbo-preview")
.withFunctionCallbacks(
List.of(new FunctionCallbackWrapper<>("getCurrentWeather", "Get the weather in location",
(response) -> "" + response.temp() + response.unit(), new MockWeatherService())))
.build();
ChatResponse response = openAiChatClient.call(new Prompt(messages, promptOptions));

View File

@@ -49,11 +49,11 @@ public class MockWeatherService implements Function<MockWeatherService.Request,
/**
* Celsius.
*/
c("metric"),
C("metric"),
/**
* Fahrenheit.
*/
f("imperial");
F("imperial");
/**
* Human readable unit name.
@@ -87,7 +87,7 @@ public class MockWeatherService implements Function<MockWeatherService.Request,
temperature = 30;
}
return new Response(temperature, 15, 20, 2, 53, 45, Unit.c);
return new Response(temperature, 15, 20, 2, 53, 45, Unit.C);
}
}

View File

@@ -52,6 +52,7 @@ public class OpenAiApiToolFunctionCallIT {
OpenAiApi completionApi = new OpenAiApi(System.getenv("OPENAI_API_KEY"));
@SuppressWarnings("null")
@Test
public void toolFunctionCall() {
@@ -80,7 +81,7 @@ public class OpenAiApiToolFunctionCallIT {
},
"unit": {
"type": "string",
"enum": ["c", "f"]
"enum": ["C", "F"]
}
},
"required": ["location", "lat", "lon", "unit"]
@@ -98,7 +99,7 @@ public class OpenAiApiToolFunctionCallIT {
List<ChatCompletionMessage> messages = new ArrayList<>(List.of(message));
ChatCompletionRequest chatCompletionRequest = new ChatCompletionRequest(messages, "gpt-4-1106-preview",
ChatCompletionRequest chatCompletionRequest = new ChatCompletionRequest(messages, "gpt-4-turbo-preview",
List.of(functionTool), null);
ResponseEntity<ChatCompletion> chatCompletion = completionApi.chatCompletionEntity(chatCompletionRequest);
@@ -132,7 +133,7 @@ public class OpenAiApiToolFunctionCallIT {
}
}
var functionResponseRequest = new ChatCompletionRequest(messages, "gpt-4-1106-preview", 0.8f);
var functionResponseRequest = new ChatCompletionRequest(messages, "gpt-4-turbo-preview", 0.8f);
ResponseEntity<ChatCompletion> chatCompletion2 = completionApi
.chatCompletionEntity(functionResponseRequest);
@@ -143,7 +144,7 @@ public class OpenAiApiToolFunctionCallIT {
assertThat(chatCompletion2.getBody().choices().get(0).message().role()).isEqualTo(Role.ASSISTANT);
assertThat(chatCompletion2.getBody().choices().get(0).message().content()).contains("San Francisco")
.containsAnyOf("30.0°F", "30°F");
.containsAnyOf("30.0°C", "30°C");
assertThat(chatCompletion2.getBody().choices().get(0).message().content()).contains("Tokyo")
.containsAnyOf("10.0°C", "10°C");
;

View File

@@ -26,7 +26,7 @@ import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.util.Assert;
/**
* Abstract implementation of the {@link ToolFunctionCallback} for interacting with the
* Abstract implementation of the {@link FunctionCallback} for interacting with the
* Model's function calling protocol and a {@link Function} wrapping the interaction with
* the 3rd party service/function.
*
@@ -40,7 +40,7 @@ import org.springframework.util.Assert;
* @param <O> the 3rd party service output type.
* @author Christian Tzolov
*/
public abstract class AbstractToolFunctionCallback<I, O> implements Function<I, O>, ToolFunctionCallback {
abstract class AbstractFunctionCallback<I, O> implements Function<I, O>, FunctionCallback {
private final String name;
@@ -55,8 +55,8 @@ public abstract class AbstractToolFunctionCallback<I, O> implements Function<I,
private final Function<O, String> responseConverter;
/**
* Constructs a new {@link AbstractToolFunctionCallback} with the given name,
* description, input type and object mapper.
* Constructs a new {@link AbstractFunctionCallback} with the given name, description,
* input type and object mapper.
* @param name Function name. Should be unique within the ChatClient's function
* registry.
* @param description Function description. Used as a "system prompt" by the model to
@@ -64,13 +64,13 @@ public abstract class AbstractToolFunctionCallback<I, O> implements Function<I,
* @param inputType Used to compute, the argument's JSON schema required by the
* Model's function calling protocol.
*/
protected AbstractToolFunctionCallback(String name, String description, Class<I> inputType) {
protected AbstractFunctionCallback(String name, String description, Class<I> inputType) {
this(name, description, inputType, Object::toString);
}
/**
* Constructs a new {@link AbstractToolFunctionCallback} with the given name,
* description, input type and object mapper.
* Constructs a new {@link AbstractFunctionCallback} with the given name, description,
* input type and object mapper.
* @param name Function name. Should be unique within the ChatClient's function
* registry.
* @param description Function description. Used as a "system prompt" by the model to
@@ -79,15 +79,15 @@ public abstract class AbstractToolFunctionCallback<I, O> implements Function<I,
* Model's function calling protocol.
* @param responseConverter Used to convert the function's output type to a string.
*/
protected AbstractToolFunctionCallback(String name, String description, Class<I> inputType,
protected AbstractFunctionCallback(String name, String description, Class<I> inputType,
Function<O, String> responseConverter) {
this(name, description, inputType, responseConverter,
new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false));
}
/**
* Constructs a new {@link AbstractToolFunctionCallback} with the given name,
* description, input type and default object mapper.
* Constructs a new {@link AbstractFunctionCallback} with the given name, description,
* input type and default object mapper.
* @param name Function name. Should be unique within the ChatClient's function
* registry.
* @param description Function description. Used as a "system prompt" by the model to
@@ -98,7 +98,7 @@ public abstract class AbstractToolFunctionCallback<I, O> implements Function<I,
* @param objectMapper Used to convert the function's input and output types to and
* from JSON.
*/
protected AbstractToolFunctionCallback(String name, String description, Class<I> inputType,
protected AbstractFunctionCallback(String name, String description, Class<I> inputType,
Function<O, String> responseConverter, ObjectMapper objectMapper) {
Assert.notNull(name, "Name must not be null");
Assert.notNull(description, "Description must not be null");
@@ -113,8 +113,7 @@ 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) {
public static <I, O> AbstractFunctionCallback<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");
@@ -123,7 +122,7 @@ public abstract class AbstractToolFunctionCallback<I, O> implements Function<I,
final Class<I> inputClassType = (Class<I>) TypeResolverHelper
.getFunctionInputClass((Class<Function<I, O>>) function.getClass());
return new DefaultToolFunctionCallback<I, O>(name, description, inputClassType, function);
return new FunctionCallbackWrapper<I, O>(name, description, inputClassType, function);
}
@Override
@@ -178,7 +177,7 @@ public abstract class AbstractToolFunctionCallback<I, O> implements Function<I,
return false;
if (getClass() != obj.getClass())
return false;
AbstractToolFunctionCallback other = (AbstractToolFunctionCallback) obj;
AbstractFunctionCallback other = (AbstractFunctionCallback) obj;
if (name == null) {
if (other.name != null)
return false;

View File

@@ -22,7 +22,7 @@ package org.springframework.ai.model.function;
*
* @author Christian Tzolov
*/
public interface ToolFunctionCallback {
public interface FunctionCallback {
/**
* @return Returns the Function name. Unique within the model.

View File

@@ -33,8 +33,7 @@ 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}.
* a {@link Function} from the Spring context and wrap it into a {@link FunctionCallback}.
*
* The name of the function is determined by the bean name.
*
@@ -48,7 +47,7 @@ import org.springframework.util.StringUtils;
* @author Christian Tzolov
* @author Christopher Smith
*/
public class SpringAiFunctionContextManager implements ApplicationContextAware {
public class FunctionCallbackContext implements ApplicationContextAware {
private GenericApplicationContext applicationContext;
@@ -58,7 +57,7 @@ public class SpringAiFunctionContextManager implements ApplicationContextAware {
}
@SuppressWarnings({ "rawtypes", "unchecked" })
public ToolFunctionCallback getFunctionFromBean(@NonNull String beanName, @Nullable String defaultDescription) {
public FunctionCallback getFunctionCallback(@NonNull String beanName, @Nullable String defaultDescription) {
Type beanType = FunctionContextUtils.findType(this.applicationContext.getBeanFactory(), beanName);
@@ -105,7 +104,7 @@ public class SpringAiFunctionContextManager implements ApplicationContextAware {
Object bean = this.applicationContext.getBean(beanName);
if (bean instanceof Function<?, ?> function) {
return new DefaultToolFunctionCallback(functionName, functionDescription, functionInputClass, function);
return new FunctionCallbackWrapper(functionName, functionDescription, functionInputClass, function);
}
else {
throw new IllegalArgumentException("Bean must be of type Function");

View File

@@ -11,28 +11,28 @@ import org.springframework.util.Assert;
* implementation to override this.
*
*/
public class DefaultToolFunctionCallback<I, O> extends AbstractToolFunctionCallback<I, O> {
public class FunctionCallbackWrapper<I, O> extends AbstractFunctionCallback<I, O> {
private Function<I, O> function;
public DefaultToolFunctionCallback(String name, String description, Class<I> inputType, Function<I, O> function) {
public FunctionCallbackWrapper(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,
public FunctionCallbackWrapper(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) {
public FunctionCallbackWrapper(String name, String description, Function<I, O> function) {
this(name, description, resolveInputType(function), function);
}
public DefaultToolFunctionCallback(String name, String description, Function<O, String> responseConverter,
public FunctionCallbackWrapper(String name, String description, Function<O, String> responseConverter,
Function<I, O> function) {
this(name, description, resolveInputType(function), responseConverter, function);
}

View File

@@ -11,7 +11,7 @@ In general the custom functions need to provide a function `name`, function `des
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/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.
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/FunctionCallback.java[FunctionCallback.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/FunctionCallbackWrapper.java[FunctionCallbackWrapper.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`.
@@ -42,9 +42,9 @@ public class MockWeatherService implements Function<Request, Response> {
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
==== FunctionCallback Wrapper
One way to register a function is to create `DefaultToolFunctionCallback` wrapper like this:
One way to register a function is to create `FunctionCallbackWrapper` wrapper like this:
[source,java]
----
@@ -52,9 +52,9 @@ One way to register a function is to create `DefaultToolFunctionCallback` wrappe
static class Config {
@Bean
public ToolFunctionCallback weatherFunctionInfo() {
public FunctionCallback weatherFunctionInfo() {
return new DefaultToolFunctionCallback<>("CurrentWeather", // (1) function name
return new FunctionCallbackWrapper<>("CurrentWeather", // (1) function name
"Get the weather in location", // (2) function description
(response) -> "" + response.temp() + response.unit(), // (3) Response Converter
new MockWeatherService()); // function code
@@ -66,7 +66,7 @@ static class Config {
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.
NOTE: The `FunctionCallbackWrapper` 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:
@@ -93,22 +93,12 @@ 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/ToolCallWithDefaultToolFunctionCallbackIT.java[ToolCallWithDefaultToolFunctionCallbackIT.java] test demo this approach.
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/FunctionCallbackWrapperIT.java[FunctionCallbackWrapperIT.java] test demo this approach.
==== 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
----
Instead of creating a `FunctionCallbackWrapper` wrapper you can register any plain `java.util.Function<I,O>` as a function calling candidate in the `ChatClient`:
[source,java]
----
@@ -127,27 +117,7 @@ static class Config {
The `@Description` annotation is optional and provides a function description (2) that helps the model to understand when to call the function.
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:
----
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:
Another options is to use the `@JacksonDescription` annotation on the `MockWeatherService.Request` to provide the function description:
[source,java]
----
@@ -165,9 +135,10 @@ static class Config {
@JsonClassDescription("Get the weather in location") // (2) function description
public record Request(String location, Unit unit) {}
----
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/FunctionCallbackWithPlainFunctionBeanIT.java[FunctionCallbackWithPlainFunctionBeanIT.java] test demo this approach.
=== Register/Call Functions with Prompt Options
In addition to the auto-configuration you can register callback functions, dynamically, with your Prompt requests:
@@ -179,22 +150,47 @@ 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 DefaultToolFunctionCallback<>(
.withFunctionCallbacks(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));
logger.info("Response: {}", response);
----
NOTE: The in-prompt registered functions are enabled by default for the duration of this request.
This approach allows to dynamically chose different functions to be called based on the user input.
The https://github.com/spring-projects/spring-ai/blob/main/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/ToolCallWithPromptFunctionRegistrationIT.java[ToolCallWithPromptFunctionRegistrationIT.java] integration test provides a complete example of how to register a function with the `OpenAiChatClient` and use it in a prompt request.
The https://github.com/spring-projects/spring-ai/blob/main/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackInPromptIT.java[FunctionCallbackInPromptIT.java] integration test provides a complete example of how to register a function with the `OpenAiChatClient` and use it in a prompt request.
=== Register Functions with Default Options
You can programmatically register functions with the `OpenAiChatClient` using the `OpenAiChatOptions#withFunctionCallbacks`:
[source,java]
----
OpenAiApi openaiApi = new OpenAiApi(apiKey);
var defaultOptions = OpenAiChatOptions.builder()
.withFunctionCallbacks(List.of(new DefaultToolFunctionCallback<>(
"CurrentWeather", // name
"Get the weather in location", // function description
new MockWeatherService()))) // function code
.build();
OpenAiChatClient chatClient = new OpenAiChatClient(openaiApi, defaultOptions);
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("CurrentWeather").build())); // Enable the function
----
NOTE: Functions are registered when OpenAiChatClient is created, by you must enable in the Prompt the functions to be used in the request.
=== Function Calling Flow

View File

@@ -73,8 +73,7 @@ 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. | -
| 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 functionCallbacks registry. | -
|====
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

@@ -62,7 +62,7 @@ The prefix `spring.ai.openai.embedding` is property prefix that configures the `
| spring.ai.openai.embedding.base-url | Optional overrides the spring.ai.openai.base-url to provide embedding specific url | -
| spring.ai.openai.embedding.api-key | Optional overrides the spring.ai.openai.api-key to provide embedding specific api-key | -
| spring.ai.openai.embedding.metadata-mode | Document content extraction mode. | EMBED
| spring.ai.openai.embedding.options.model | The model to use | text-embedding-ada-002
| spring.ai.openai.embedding.options.model | The model to use | text-embedding-3-large (other options: text-embedding-3-small, text-embedding-ada-002)
| spring.ai.openai.embedding.options.encodingFormat | The format to return the embeddings in. Can be either float or base64. | -
| spring.ai.openai.embedding.options.user | A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. | -
|====

View File

@@ -20,8 +20,8 @@ import java.util.List;
import org.springframework.ai.autoconfigure.NativeHints;
import org.springframework.ai.embedding.EmbeddingClient;
import org.springframework.ai.model.function.SpringAiFunctionContextManager;
import org.springframework.ai.model.function.ToolFunctionCallback;
import org.springframework.ai.model.function.FunctionCallbackContext;
import org.springframework.ai.model.function.FunctionCallback;
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, SpringAiFunctionContextManager functionManager) {
List<FunctionCallback> toolFunctionCallbacks, FunctionCallbackContext functionCallbackContext) {
String apiKey = StringUtils.hasText(chatProperties.getApiKey()) ? chatProperties.getApiKey()
: commonProperties.getApiKey();
@@ -72,17 +72,10 @@ public class OpenAiAutoConfiguration {
var openAiApi = new OpenAiApi(baseUrl, apiKey, restClientBuilder);
if (!CollectionUtils.isEmpty(toolFunctionCallbacks)) {
chatProperties.getOptions().getToolCallbacks().addAll(toolFunctionCallbacks);
chatProperties.getOptions().getFunctionCallbacks().addAll(toolFunctionCallbacks);
}
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());
return new OpenAiChatClient(openAiApi, chatProperties.getOptions(), functionCallbackContext);
}
@Bean
@@ -124,8 +117,8 @@ public class OpenAiAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public SpringAiFunctionContextManager springAiFunctionManager(ApplicationContext context) {
SpringAiFunctionContextManager manager = new SpringAiFunctionContextManager();
public FunctionCallbackContext springAiFunctionManager(ApplicationContext context) {
FunctionCallbackContext manager = new FunctionCallbackContext();
manager.setApplicationContext(context);
return manager;
}

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.function.DefaultToolFunctionCallback;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.ai.openai.OpenAiChatClient;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.boot.autoconfigure.AutoConfigurations;
@@ -37,9 +37,9 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*")
public class ToolCallWithPromptFunctionRegistrationIT {
public class FunctionCallbackInPromptIT {
private final Logger logger = LoggerFactory.getLogger(ToolCallWithPromptFunctionRegistrationIT.class);
private final Logger logger = LoggerFactory.getLogger(FunctionCallbackInPromptIT.class);
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY"))
@@ -47,14 +47,14 @@ public class ToolCallWithPromptFunctionRegistrationIT {
@Test
void functionCallTest() {
contextRunner.withPropertyValues("spring.ai.openai.chat.options.model=gpt-4-1106-preview").run(context -> {
contextRunner.withPropertyValues("spring.ai.openai.chat.options.model=gpt-4-turbo-preview").run(context -> {
OpenAiChatClient chatClient = context.getBean(OpenAiChatClient.class);
UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?");
var promptOptions = OpenAiChatOptions.builder()
.withToolCallbacks(List.of(new DefaultToolFunctionCallback<>("CurrentWeatherService", // name
.withFunctionCallbacks(List.of(new FunctionCallbackWrapper<>("CurrentWeatherService", // name
"Get the weather in location", // function description
(response) -> "" + response.temp() + response.unit(), // responseConverter
new MockWeatherService()))) // function code

View File

@@ -40,9 +40,9 @@ import org.springframework.context.annotation.Description;
import static org.assertj.core.api.Assertions.assertThat;
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*")
class ToolCallWithPlainBeanRegistrationIT {
class FunctionCallbackWithPlainFunctionBeanIT {
private final Logger logger = LoggerFactory.getLogger(ToolCallWithPlainBeanRegistrationIT.class);
private final Logger logger = LoggerFactory.getLogger(FunctionCallbackWithPlainFunctionBeanIT.class);
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY"))
@@ -51,40 +51,27 @@ class ToolCallWithPlainBeanRegistrationIT {
@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 -> {
contextRunner.withPropertyValues("spring.ai.openai.chat.options.model=gpt-4-turbo-preview").run(context -> {
OpenAiChatClient chatClient = context.getBean(OpenAiChatClient.class);
OpenAiChatClient chatClient = context.getBean(OpenAiChatClient.class);
UserMessage userMessage = new UserMessage(
"What's the weather like in San Francisco, Tokyo, and Paris?");
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()));
ChatResponse response = chatClient.call(new Prompt(List.of(userMessage),
OpenAiChatOptions.builder().withEnabledFunction("weatherFunction").build()));
logger.info("Response: {}", response);
logger.info("Response: {}", response);
assertThat(response.getResult().getOutput().getContent()).contains("30", "10", "15");
assertThat(response.getResult().getOutput().getContent()).contains("30", "10", "15");
response = chatClient.call(new Prompt(List.of(userMessage),
OpenAiChatOptions.builder().withEnabledFunction("weatherFunction2").build()));
response = chatClient.call(new Prompt(List.of(userMessage),
OpenAiChatOptions.builder().withEnabledFunction("weatherFunction3").build()));
logger.info("Response: {}", response);
logger.info("Response: {}", response);
assertThat(response.getResult().getOutput().getContent()).contains("30", "10", "15");
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
@@ -93,14 +80,7 @@ class ToolCallWithPlainBeanRegistrationIT {
@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);
return new MockWeatherService();
}
// Relies on the Request's JsonClassDescription annotation to provide the

View File

@@ -27,8 +27,8 @@ 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.model.function.FunctionCallbackWrapper;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.openai.OpenAiChatClient;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.boot.autoconfigure.AutoConfigurations;
@@ -40,9 +40,9 @@ import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*")
public class TollCallWithDefaultToolFunctionCallbackIT {
public class FunctionCallbackWrapperIT {
private final Logger logger = LoggerFactory.getLogger(TollCallWithDefaultToolFunctionCallbackIT.class);
private final Logger logger = LoggerFactory.getLogger(FunctionCallbackWrapperIT.class);
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY"))
@@ -51,7 +51,7 @@ public class TollCallWithDefaultToolFunctionCallbackIT {
@Test
void functionCallTest() {
contextRunner.withPropertyValues("spring.ai.openai.chat.options.model=gpt-4-1106-preview").run(context -> {
contextRunner.withPropertyValues("spring.ai.openai.chat.options.model=gpt-4-turbo-preview").run(context -> {
OpenAiChatClient chatClient = context.getBean(OpenAiChatClient.class);
@@ -71,9 +71,9 @@ public class TollCallWithDefaultToolFunctionCallbackIT {
static class Config {
@Bean
public ToolFunctionCallback weatherFunctionInfo() {
public FunctionCallback weatherFunctionInfo() {
return new DefaultToolFunctionCallback<>("WeatherInfo", // function name
return new FunctionCallbackWrapper<>("WeatherInfo", // function name
"Get the weather in location", // function description
(response) -> "" + response.temp() + response.unit(), // responseConverter
new MockWeatherService()); // function code

View File

@@ -25,6 +25,8 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
/**
* Mock 3rd party weather service.
*
* @author Christian Tzolov
*/
public class MockWeatherService implements Function<MockWeatherService.Request, MockWeatherService.Response> {
@@ -49,11 +51,11 @@ public class MockWeatherService implements Function<MockWeatherService.Request,
/**
* Celsius.
*/
c("metric"),
C("metric"),
/**
* Fahrenheit.
*/
f("imperial");
F("imperial");
/**
* Human readable unit name.
@@ -87,7 +89,7 @@ public class MockWeatherService implements Function<MockWeatherService.Request,
temperature = 30;
}
return new Response(temperature, 15, 20, 2, 53, 45, Unit.c);
return new Response(temperature, 15, 20, 2, 53, 45, Unit.C);
}
}