Simplify Function Calling API
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
;
|
||||
|
||||
@@ -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;
|
||||
@@ -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.
|
||||
@@ -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");
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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. | -
|
||||
|====
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user