From d580fe2757b7c05b7b33d1656514fafeb7985979 Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Thu, 15 Feb 2024 20:26:31 +0100 Subject: [PATCH] Simplify Function Calling API --- .../ai/openai/OpenAiChatClient.java | 90 ++++++++++++++----- .../ai/openai/OpenAiChatOptions.java | 56 +++--------- .../ai/openai/OpenAiEmbeddingClient.java | 2 +- .../ai/openai/api/OpenAiApi.java | 3 +- .../ai/openai/ChatCompletionRequestTests.java | 81 +++++++---------- .../ai/openai/chat/OpenAiChatClientIT.java | 20 ++--- .../chat/api/tool/MockWeatherService.java | 6 +- .../api/tool/OpenAiApiToolFunctionCallIT.java | 9 +- ...ack.java => AbstractFunctionCallback.java} | 29 +++--- ...ionCallback.java => FunctionCallback.java} | 2 +- ...ager.java => FunctionCallbackContext.java} | 9 +- ...back.java => FunctionCallbackWrapper.java} | 10 +-- .../functions/openai-chat-functions.adoc | 84 +++++++++-------- .../ROOT/pages/api/clients/openai-chat.adoc | 3 +- .../api/embeddings/openai-embeddings.adoc | 2 +- .../openai/OpenAiAutoConfiguration.java | 21 ++--- ...T.java => FunctionCallbackInPromptIT.java} | 10 +-- ...ctionCallbackWithPlainFunctionBeanIT.java} | 50 ++++------- ...IT.java => FunctionCallbackWrapperIT.java} | 14 +-- .../openai/tool/MockWeatherService.java | 8 +- 20 files changed, 237 insertions(+), 272 deletions(-) rename spring-ai-core/src/main/java/org/springframework/ai/model/function/{AbstractToolFunctionCallback.java => AbstractFunctionCallback.java} (83%) rename spring-ai-core/src/main/java/org/springframework/ai/model/function/{ToolFunctionCallback.java => FunctionCallback.java} (97%) rename spring-ai-core/src/main/java/org/springframework/ai/model/function/{SpringAiFunctionContextManager.java => FunctionCallbackContext.java} (92%) rename spring-ai-core/src/main/java/org/springframework/ai/model/function/{DefaultToolFunctionCallback.java => FunctionCallbackWrapper.java} (72%) rename spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/{ToolCallWithPromptFunctionRegistrationIT.java => FunctionCallbackInPromptIT.java} (88%) rename spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/{ToolCallWithPlainBeanRegistrationIT.java => FunctionCallbackWithPlainFunctionBeanIT.java} (59%) rename spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/{TollCallWithDefaultToolFunctionCallbackIT.java => FunctionCallbackWrapperIT.java} (85%) diff --git a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatClient.java b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatClient.java index c7d608ad1..15e1ef20e 100644 --- a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatClient.java +++ b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatClient.java @@ -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 toolCallbackRegister = new ConcurrentHashMap<>(); + /** + * The function callback register is used to resolve the function callbacks by name. + */ + private Map 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 promptEnabledFunctions = handleToolFunctionConfigurations(updatedRuntimeOptions, true, + Set promptEnabledFunctions = handleFunctionCallbackConfigurations(updatedRuntimeOptions, true, true); enabledFunctionsForRequest.addAll(promptEnabledFunctions); @@ -202,7 +237,8 @@ public class OpenAiChatClient implements ChatClient, StreamingChatClient { if (this.defaultOptions != null) { - Set defaultEnabledFunctions = handleToolFunctionConfigurations(this.defaultOptions, false, false); + Set defaultEnabledFunctions = handleFunctionCallbackConfigurations(this.defaultOptions, false, + false); enabledFunctionsForRequest.addAll(defaultEnabledFunctions); @@ -224,26 +260,26 @@ public class OpenAiChatClient implements ChatClient, StreamingChatClient { return request; } - private Set handleToolFunctionConfigurations(OpenAiChatOptions options, boolean autoEnableCallbackFunctions, - boolean overrideCallbackFunctionsRegister) { + private Set handleFunctionCallbackConfigurations(OpenAiChatOptions options, + boolean autoEnableCallbackFunctions, boolean overrideCallbackFunctionsRegister) { Set 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 getToolCallbackRegister() { - return toolCallbackRegister; + Map getFunctionCallbackRegister() { + return functionCallbackRegister; } - public List getFunctionTools(Set functionNames) { + private List getFunctionTools(Set functionNames) { List 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)); diff --git a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatOptions.java b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatOptions.java index e825d8884..5168b785c 100644 --- a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatOptions.java +++ b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatOptions.java @@ -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 toolCallbacks = new ArrayList<>(); + private List 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 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 beanFunctions = new HashMap<>(); // @formatter:on public static Builder builder() { @@ -246,8 +234,8 @@ public class OpenAiChatOptions implements ChatOptions { return this; } - public Builder withToolCallbacks(List toolCallbacks) { - this.options.toolCallbacks = toolCallbacks; + public Builder withFunctionCallbacks(List functionCallbacks) { + this.options.functionCallbacks = functionCallbacks; return this; } @@ -263,16 +251,6 @@ public class OpenAiChatOptions implements ChatOptions { return this; } - public Builder withBeanFunctions(Map 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 getToolCallbacks() { - return this.toolCallbacks; + public List getFunctionCallbacks() { + return this.functionCallbacks; } - public void setToolCallbacks(List toolCallbacks) { - this.toolCallbacks = toolCallbacks; + public void setFunctionCallbacks(List functionCallbacks) { + this.functionCallbacks = functionCallbacks; } public Set getEnabledFunctions() { @@ -411,14 +389,6 @@ public class OpenAiChatOptions implements ChatOptions { this.enabledFunctions = functionNames; } - public Map getBeanFunctions() { - return beanFunctions; - } - - public void setBeanFunctions(Map beanFunctions) { - this.beanFunctions = beanFunctions; - } - @Override public int hashCode() { final int prime = 31; diff --git a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiEmbeddingClient.java b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiEmbeddingClient.java index 2416ee095..6be3d52de 100644 --- a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiEmbeddingClient.java +++ b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiEmbeddingClient.java @@ -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; diff --git a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiApi.java b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiApi.java index cfae84b3a..37cc23abb 100644 --- a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiApi.java +++ b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiApi.java @@ -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 SSE_DONE_PREDICATE = "[DONE]"::equals; private final RestClient restClient; diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/ChatCompletionRequestTests.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/ChatCompletionRequestTests.java index 0b55a763f..fad3f7d1b 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/ChatCompletionRequestTests.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/ChatCompletionRequestTests.java @@ -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( - 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( - 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(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"); } diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatClientIT.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatClientIT.java index 1562dedba..a664e4562 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatClientIT.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatClientIT.java @@ -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 messages = new ArrayList<>(List.of(userMessage)); var promptOptions = OpenAiChatOptions.builder() - .withModel("gpt-4-1106-preview") - .withToolCallbacks( - List.of(new AbstractToolFunctionCallback( - "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)); diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/api/tool/MockWeatherService.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/api/tool/MockWeatherService.java index 62c2afe13..c80aabf63 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/api/tool/MockWeatherService.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/api/tool/MockWeatherService.java @@ -49,11 +49,11 @@ public class MockWeatherService implements Function 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 = 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 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"); ; diff --git a/spring-ai-core/src/main/java/org/springframework/ai/model/function/AbstractToolFunctionCallback.java b/spring-ai-core/src/main/java/org/springframework/ai/model/function/AbstractFunctionCallback.java similarity index 83% rename from spring-ai-core/src/main/java/org/springframework/ai/model/function/AbstractToolFunctionCallback.java rename to spring-ai-core/src/main/java/org/springframework/ai/model/function/AbstractFunctionCallback.java index 760b18c4d..6714b7d86 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/model/function/AbstractToolFunctionCallback.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/model/function/AbstractFunctionCallback.java @@ -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 the 3rd party service output type. * @author Christian Tzolov */ -public abstract class AbstractToolFunctionCallback implements Function, ToolFunctionCallback { +abstract class AbstractFunctionCallback implements Function, FunctionCallback { private final String name; @@ -55,8 +55,8 @@ public abstract class AbstractToolFunctionCallback implements Function 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 implements Function inputType) { + protected AbstractFunctionCallback(String name, String description, Class 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 implements Function inputType, + protected AbstractFunctionCallback(String name, String description, Class inputType, Function 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 implements Function inputType, + protected AbstractFunctionCallback(String name, String description, Class inputType, Function 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 implements Function AbstractToolFunctionCallback of(String name, String description, - Function function) { + public static AbstractFunctionCallback of(String name, String description, Function 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 implements Function inputClassType = (Class) TypeResolverHelper .getFunctionInputClass((Class>) function.getClass()); - return new DefaultToolFunctionCallback(name, description, inputClassType, function); + return new FunctionCallbackWrapper(name, description, inputClassType, function); } @Override @@ -178,7 +177,7 @@ public abstract class AbstractToolFunctionCallback implements 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"); diff --git a/spring-ai-core/src/main/java/org/springframework/ai/model/function/DefaultToolFunctionCallback.java b/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallbackWrapper.java similarity index 72% rename from spring-ai-core/src/main/java/org/springframework/ai/model/function/DefaultToolFunctionCallback.java rename to spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallbackWrapper.java index 584364b79..12e362e3e 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/model/function/DefaultToolFunctionCallback.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallbackWrapper.java @@ -11,28 +11,28 @@ import org.springframework.util.Assert; * implementation to override this. * */ -public class DefaultToolFunctionCallback extends AbstractToolFunctionCallback { +public class FunctionCallbackWrapper extends AbstractFunctionCallback { private Function function; - public DefaultToolFunctionCallback(String name, String description, Class inputType, Function function) { + public FunctionCallbackWrapper(String name, String description, Class inputType, Function function) { super(name, description, inputType); Assert.notNull(function, "Function must not be null"); this.function = function; } - public DefaultToolFunctionCallback(String name, String description, Class inputType, + public FunctionCallbackWrapper(String name, String description, Class inputType, Function responseConverter, Function function) { super(name, description, inputType, responseConverter); Assert.notNull(function, "Function must not be null"); this.function = function; } - public DefaultToolFunctionCallback(String name, String description, Function function) { + public FunctionCallbackWrapper(String name, String description, Function function) { this(name, description, resolveInputType(function), function); } - public DefaultToolFunctionCallback(String name, String description, Function responseConverter, + public FunctionCallbackWrapper(String name, String description, Function responseConverter, Function function) { this(name, description, resolveInputType(function), responseConverter, function); } diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/clients/functions/openai-chat-functions.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/clients/functions/openai-chat-functions.adoc index b0cd2af60..f0dd95b2c 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/clients/functions/openai-chat-functions.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/clients/functions/openai-chat-functions.adoc @@ -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 beans definition as function calling candidates in the `ChatClient`. @@ -42,9 +42,9 @@ public class MockWeatherService implements Function { 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` 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.` 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` 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.=` property: - ----- -spring.ai.openai.chat.options.beanFunctions.currentWeather2=Get the weather in location ----- - -[source,java] ----- -@Configuration -static class Config { - - @Bean - public Function 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 diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/clients/openai-chat.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/clients/openai-chat.adoc index 02f0f3dd4..7f04768e8 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/clients/openai-chat.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/clients/openai-chat.adoc @@ -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.. | 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. diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/openai-embeddings.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/openai-embeddings.adoc index 67739292f..1bf618e60 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/openai-embeddings.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/openai-embeddings.adoc @@ -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. | - |==== diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiAutoConfiguration.java index e973f13ef..791347fb7 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiAutoConfiguration.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiAutoConfiguration.java @@ -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 toolFunctionCallbacks, SpringAiFunctionContextManager functionManager) { + List 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; } diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/ToolCallWithPromptFunctionRegistrationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackInPromptIT.java similarity index 88% rename from spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/ToolCallWithPromptFunctionRegistrationIT.java rename to spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackInPromptIT.java index 6af1f71e2..7859396bd 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/ToolCallWithPromptFunctionRegistrationIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackInPromptIT.java @@ -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 diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/ToolCallWithPlainBeanRegistrationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWithPlainFunctionBeanIT.java similarity index 59% rename from spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/ToolCallWithPlainBeanRegistrationIT.java rename to spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWithPlainFunctionBeanIT.java index 0e76768ee..3e4308216 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/ToolCallWithPlainBeanRegistrationIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWithPlainFunctionBeanIT.java @@ -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 weatherFunction() { - MockWeatherService weatherService = new MockWeatherService(); - return (weatherService::apply); - } - - @Bean(name = "weatherFunction2") - public Function weatherFunction1() { - MockWeatherService weatherService = new MockWeatherService(); - return (weatherService::apply); + return new MockWeatherService(); } // Relies on the Request's JsonClassDescription annotation to provide the diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/TollCallWithDefaultToolFunctionCallbackIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWrapperIT.java similarity index 85% rename from spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/TollCallWithDefaultToolFunctionCallbackIT.java rename to spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWrapperIT.java index 607e28d94..cfb77440d 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/TollCallWithDefaultToolFunctionCallbackIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWrapperIT.java @@ -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 diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/MockWeatherService.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/MockWeatherService.java index e40085651..b3df54c4b 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/MockWeatherService.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/MockWeatherService.java @@ -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 { @@ -49,11 +51,11 @@ public class MockWeatherService implements Function