From 7c800f35b8c590c4ec44ba0cd3afb8ccd1be2e18 Mon Sep 17 00:00:00 2001 From: kamosama <837080904@qq.com> Date: Tue, 23 Jul 2024 18:11:55 +0800 Subject: [PATCH] Fix a FunctionCallback inside a container that pollutes the global model's ChatOptions The autoconfiguration adds the FunctionCallbacks directly to the model's ChatOptions, which results in the FunctionCallback being included in the request each time it is called. The modification registers the container's FunctionCallback directly to the model's functionCallbackRegister using the parent AbstractToolCallSupport constsructor. Replace the handleFunctionCallbackConfigurations by simplified runtimeFunctionCallbackConfigurations. Co-authored-by Christian Tzolov --- .../ai/anthropic/AnthropicChatModel.java | 34 ++++++--- .../ai/azure/openai/AzureOpenAiChatModel.java | 22 +++--- .../ai/mistralai/MistralAiChatModel.java | 24 +++--- .../ai/ollama/OllamaChatModel.java | 32 +++----- .../ai/openai/OpenAiChatModel.java | 48 +++++++----- .../ai/openai/OpenAiTestConfiguration.java | 4 +- .../chat/OpenAiPaymentTransactionIT.java | 2 +- .../gemini/VertexAiGeminiChatModel.java | 29 ++++--- .../chat/model/AbstractToolCallSupport.java | 75 +++++++++++++------ .../anthropic/AnthropicAutoConfiguration.java | 9 +-- .../openai/AzureOpenAiAutoConfiguration.java | 23 +++--- .../minimax/MiniMaxAutoConfiguration.java | 12 ++- .../mistralai/MistralAiAutoConfiguration.java | 7 +- .../ollama/OllamaAutoConfiguration.java | 7 +- .../openai/OpenAiAutoConfiguration.java | 8 +- .../VertexAiGeminiAutoConfiguration.java | 14 ++-- .../zhipuai/ZhiPuAiAutoConfiguration.java | 12 ++- .../FunctionCallWithFunctionWrapperIT.java | 4 +- .../ollama/OllamaChatAutoConfigurationIT.java | 2 +- .../tool/FunctionCallbackInPrompt2IT.java | 40 +++++----- .../tool/FunctionCallbackInPromptIT.java | 9 +-- ...nctionCallbackWithPlainFunctionBeanIT.java | 45 ++++++----- .../tool/FunctionCallbackWrapper2IT.java | 12 ++- .../tool/FunctionCallbackWrapperIT.java | 4 +- spring-ai-test/pom.xml | 6 ++ 25 files changed, 263 insertions(+), 221 deletions(-) diff --git a/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/AnthropicChatModel.java b/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/AnthropicChatModel.java index 23e9a4fda..7ab7c72ae 100644 --- a/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/AnthropicChatModel.java +++ b/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/AnthropicChatModel.java @@ -46,6 +46,7 @@ import org.springframework.ai.chat.model.Generation; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.model.ModelOptionsUtils; +import org.springframework.ai.model.function.FunctionCallback; import org.springframework.ai.model.function.FunctionCallbackContext; import org.springframework.ai.retry.RetryUtils; import org.springframework.http.ResponseEntity; @@ -134,7 +135,24 @@ public class AnthropicChatModel extends AbstractToolCallSupport implements ChatM public AnthropicChatModel(AnthropicApi anthropicApi, AnthropicChatOptions defaultOptions, RetryTemplate retryTemplate, FunctionCallbackContext functionCallbackContext) { - super(functionCallbackContext); + this(anthropicApi, defaultOptions, retryTemplate, functionCallbackContext, List.of()); + } + + /** + * Construct a new {@link AnthropicChatModel} instance. + * @param anthropicApi the lower-level API for the Anthropic service. + * @param defaultOptions the default options used for the chat completion requests. + * @param retryTemplate the retry template used to retry the Anthropic API calls. + * @param functionCallbackContext the function callback context used to store the + * state of the function calls. + * @param toolFunctionCallbacks the tool function callbacks used to handle the tool + * calls. + */ + public AnthropicChatModel(AnthropicApi anthropicApi, AnthropicChatOptions defaultOptions, + RetryTemplate retryTemplate, FunctionCallbackContext functionCallbackContext, + List toolFunctionCallbacks) { + + super(functionCallbackContext, defaultOptions, toolFunctionCallbacks); Assert.notNull(anthropicApi, "AnthropicApi must not be null"); Assert.notNull(defaultOptions, "DefaultOptions must not be null"); @@ -318,21 +336,17 @@ public class AnthropicChatModel extends AbstractToolCallSupport implements ChatM AnthropicChatOptions updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(prompt.getOptions(), ChatOptions.class, AnthropicChatOptions.class); - Set promptEnabledFunctions = this.handleFunctionCallbackConfigurations(updatedRuntimeOptions, - IS_RUNTIME_CALL); - functionsForThisRequest.addAll(promptEnabledFunctions); + functionsForThisRequest.addAll(this.runtimeFunctionCallbackConfigurations(updatedRuntimeOptions)); request = ModelOptionsUtils.merge(updatedRuntimeOptions, request, ChatCompletionRequest.class); } - if (this.defaultOptions != null) { - Set defaultEnabledFunctions = this.handleFunctionCallbackConfigurations(this.defaultOptions, - !IS_RUNTIME_CALL); - functionsForThisRequest.addAll(defaultEnabledFunctions); - - request = ModelOptionsUtils.merge(request, this.defaultOptions, ChatCompletionRequest.class); + if (!CollectionUtils.isEmpty(this.defaultOptions.getFunctions())) { + functionsForThisRequest.addAll(this.defaultOptions.getFunctions()); } + request = ModelOptionsUtils.merge(request, this.defaultOptions, ChatCompletionRequest.class); + if (!CollectionUtils.isEmpty(functionsForThisRequest)) { List tools = getFunctionTools(functionsForThisRequest); diff --git a/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiChatModel.java b/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiChatModel.java index 23fce6620..f70217e90 100644 --- a/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiChatModel.java +++ b/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiChatModel.java @@ -42,6 +42,7 @@ import org.springframework.ai.chat.model.Generation; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.model.ModelOptionsUtils; +import org.springframework.ai.model.function.FunctionCallback; import org.springframework.ai.model.function.FunctionCallbackContext; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -121,7 +122,12 @@ public class AzureOpenAiChatModel extends AbstractToolCallSupport implements Cha public AzureOpenAiChatModel(OpenAIClient microsoftOpenAiClient, AzureOpenAiChatOptions options, FunctionCallbackContext functionCallbackContext) { - super(functionCallbackContext); + this(microsoftOpenAiClient, options, functionCallbackContext, List.of()); + } + + public AzureOpenAiChatModel(OpenAIClient microsoftOpenAiClient, AzureOpenAiChatOptions options, + FunctionCallbackContext functionCallbackContext, List toolFunctionCallbacks) { + super(functionCallbackContext, options, toolFunctionCallbacks); Assert.notNull(microsoftOpenAiClient, "com.azure.ai.openai.OpenAIClient must not be null"); Assert.notNull(options, "AzureOpenAiChatOptions must not be null"); this.openAIClient = microsoftOpenAiClient; @@ -267,23 +273,17 @@ public class AzureOpenAiChatModel extends AbstractToolCallSupport implements Cha ChatCompletionsOptions options = new ChatCompletionsOptions(azureMessages); - if (this.defaultOptions != null) { + options = this.merge(options, this.defaultOptions); - options = this.merge(options, this.defaultOptions); - - Set defaultEnabledFunctions = this.handleFunctionCallbackConfigurations(this.defaultOptions, - !IS_RUNTIME_CALL); - functionsForThisRequest.addAll(defaultEnabledFunctions); - } + if (!CollectionUtils.isEmpty(this.defaultOptions.getFunctions())) + functionsForThisRequest.addAll(this.defaultOptions.getFunctions()); if (prompt.getOptions() != null) { AzureOpenAiChatOptions updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(prompt.getOptions(), ChatOptions.class, AzureOpenAiChatOptions.class); options = this.merge(updatedRuntimeOptions, options); - Set promptEnabledFunctions = this.handleFunctionCallbackConfigurations(updatedRuntimeOptions, - IS_RUNTIME_CALL); - functionsForThisRequest.addAll(promptEnabledFunctions); + functionsForThisRequest.addAll(this.runtimeFunctionCallbackConfigurations(updatedRuntimeOptions)); } // Add the enabled functions definitions to the request's tools parameter. diff --git a/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/MistralAiChatModel.java b/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/MistralAiChatModel.java index 1b9a0e161..fc4d37817 100644 --- a/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/MistralAiChatModel.java +++ b/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/MistralAiChatModel.java @@ -45,6 +45,7 @@ import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionMessage.T import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionRequest; import org.springframework.ai.mistralai.metadata.MistralAiUsage; import org.springframework.ai.model.ModelOptionsUtils; +import org.springframework.ai.model.function.FunctionCallback; import org.springframework.ai.model.function.FunctionCallbackContext; import org.springframework.ai.retry.RetryUtils; import org.springframework.http.ResponseEntity; @@ -95,7 +96,13 @@ public class MistralAiChatModel extends AbstractToolCallSupport implements ChatM public MistralAiChatModel(MistralAiApi mistralAiApi, MistralAiChatOptions options, FunctionCallbackContext functionCallbackContext, RetryTemplate retryTemplate) { - super(functionCallbackContext); + this(mistralAiApi, options, functionCallbackContext, List.of(), retryTemplate); + } + + public MistralAiChatModel(MistralAiApi mistralAiApi, MistralAiChatOptions options, + FunctionCallbackContext functionCallbackContext, List toolFunctionCallbacks, + RetryTemplate retryTemplate) { + super(functionCallbackContext, options, toolFunctionCallbacks); Assert.notNull(mistralAiApi, "MistralAiApi must not be null"); Assert.notNull(options, "Options must not be null"); Assert.notNull(retryTemplate, "RetryTemplate must not be null"); @@ -292,22 +299,17 @@ public class MistralAiChatModel extends AbstractToolCallSupport implements ChatM var request = new MistralAiApi.ChatCompletionRequest(chatCompletionMessages, stream); - if (this.defaultOptions != null) { - Set defaultEnabledFunctions = this.handleFunctionCallbackConfigurations(this.defaultOptions, - !IS_RUNTIME_CALL); - - functionsForThisRequest.addAll(defaultEnabledFunctions); - - request = ModelOptionsUtils.merge(request, this.defaultOptions, MistralAiApi.ChatCompletionRequest.class); + if (!CollectionUtils.isEmpty(this.defaultOptions.getFunctions())) { + functionsForThisRequest.addAll(this.defaultOptions.getFunctions()); } + request = ModelOptionsUtils.merge(request, this.defaultOptions, MistralAiApi.ChatCompletionRequest.class); + if (prompt.getOptions() != null) { var updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(prompt.getOptions(), ChatOptions.class, MistralAiChatOptions.class); - Set promptEnabledFunctions = this.handleFunctionCallbackConfigurations(updatedRuntimeOptions, - IS_RUNTIME_CALL); - functionsForThisRequest.addAll(promptEnabledFunctions); + functionsForThisRequest.addAll(this.runtimeFunctionCallbackConfigurations(updatedRuntimeOptions)); request = ModelOptionsUtils.merge(updatedRuntimeOptions, request, MistralAiApi.ChatCompletionRequest.class); } diff --git a/models/spring-ai-ollama/src/main/java/org/springframework/ai/ollama/OllamaChatModel.java b/models/spring-ai-ollama/src/main/java/org/springframework/ai/ollama/OllamaChatModel.java index f2f5a168c..68d1c61e4 100644 --- a/models/spring-ai-ollama/src/main/java/org/springframework/ai/ollama/OllamaChatModel.java +++ b/models/spring-ai-ollama/src/main/java/org/springframework/ai/ollama/OllamaChatModel.java @@ -34,6 +34,7 @@ import org.springframework.ai.chat.model.Generation; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.model.ModelOptionsUtils; +import org.springframework.ai.model.function.FunctionCallback; import org.springframework.ai.model.function.FunctionCallbackContext; import org.springframework.ai.ollama.api.OllamaApi; import org.springframework.ai.ollama.api.OllamaApi.ChatRequest; @@ -83,31 +84,18 @@ public class OllamaChatModel extends AbstractToolCallSupport implements ChatMode public OllamaChatModel(OllamaApi chatApi, OllamaOptions defaultOptions, FunctionCallbackContext functionCallbackContext) { - super(functionCallbackContext); + this(chatApi, defaultOptions, functionCallbackContext, List.of()); + } + + public OllamaChatModel(OllamaApi chatApi, OllamaOptions defaultOptions, + FunctionCallbackContext functionCallbackContext, List toolFunctionCallbacks) { + super(functionCallbackContext, defaultOptions, toolFunctionCallbacks); Assert.notNull(chatApi, "OllamaApi must not be null"); Assert.notNull(defaultOptions, "DefaultOptions must not be null"); this.chatApi = chatApi; this.defaultOptions = defaultOptions; } - /** - * @deprecated Use {@link OllamaOptions#setModel} instead. - */ - @Deprecated - public OllamaChatModel withModel(String model) { - this.defaultOptions.setModel(model); - return this; - } - - /** - * @deprecated Use {@link OllamaOptions} constructor instead. - */ - @Deprecated - public OllamaChatModel withDefaultOptions(OllamaOptions options) { - this.defaultOptions = options; - return this; - } - @Override public ChatResponse call(Prompt prompt) { @@ -246,10 +234,12 @@ public class OllamaChatModel extends AbstractToolCallSupport implements ChatMode if (prompt.getOptions() != null) { runtimeOptions = ModelOptionsUtils.copyToTarget(prompt.getOptions(), ChatOptions.class, OllamaOptions.class); - functionsForThisRequest.addAll(this.handleFunctionCallbackConfigurations(runtimeOptions, IS_RUNTIME_CALL)); + functionsForThisRequest.addAll(this.runtimeFunctionCallbackConfigurations(runtimeOptions)); } - functionsForThisRequest.addAll(this.handleFunctionCallbackConfigurations(this.defaultOptions, IS_RUNTIME_CALL)); + if (!CollectionUtils.isEmpty(this.defaultOptions.getFunctions())) { + functionsForThisRequest.addAll(this.defaultOptions.getFunctions()); + } OllamaOptions mergedOptions = ModelOptionsUtils.merge(runtimeOptions, this.defaultOptions, OllamaOptions.class); // Override the model. diff --git a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatModel.java b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatModel.java index 3255e34eb..cf5b26959 100644 --- a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatModel.java +++ b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatModel.java @@ -41,6 +41,7 @@ import org.springframework.ai.chat.model.StreamingChatModel; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.model.ModelOptionsUtils; +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; @@ -131,10 +132,29 @@ public class OpenAiChatModel extends AbstractToolCallSupport implements ChatMode */ public OpenAiChatModel(OpenAiApi openAiApi, OpenAiChatOptions options, FunctionCallbackContext functionCallbackContext, RetryTemplate retryTemplate) { - super(functionCallbackContext); + this(openAiApi, options, functionCallbackContext, List.of(), retryTemplate); + } + + /** + * Initializes a new instance of the OpenAiChatModel. + * @param openAiApi The OpenAiApi instance to be used for interacting with the OpenAI + * Chat API. + * @param options The OpenAiChatOptions to configure the chat model. + * @param functionCallbackContext The function callback context. + * @param toolFunctionCallbacks The tool function callbacks. + * @param retryTemplate The retry template. + */ + public OpenAiChatModel(OpenAiApi openAiApi, OpenAiChatOptions options, + FunctionCallbackContext functionCallbackContext, List toolFunctionCallbacks, + RetryTemplate retryTemplate) { + super(functionCallbackContext, options, toolFunctionCallbacks); + Assert.notNull(openAiApi, "OpenAiApi must not be null"); Assert.notNull(options, "Options must not be null"); Assert.notNull(retryTemplate, "RetryTemplate must not be null"); + Assert.isTrue(CollectionUtils.isEmpty(options.getFunctionCallbacks()), + "The default function callbacks must be set via the toolFunctionCallbacks constructor parameter"); + this.openAiApi = openAiApi; this.defaultOptions = options; this.retryTemplate = retryTemplate; @@ -302,8 +322,6 @@ public class OpenAiChatModel extends AbstractToolCallSupport implements ChatMode */ ChatCompletionRequest createRequest(Prompt prompt, boolean stream) { - Set functionsForThisRequest = new HashSet<>(); - List chatCompletionMessages = prompt.getInstructions().stream().map(message -> { if (message.getMessageType() == MessageType.USER || message.getMessageType() == MessageType.SYSTEM) { Object content = message.getContent(); @@ -358,33 +376,29 @@ public class OpenAiChatModel extends AbstractToolCallSupport implements ChatMode ChatCompletionRequest request = new ChatCompletionRequest(chatCompletionMessages, stream); + Set enabledToolsToUse = new HashSet<>(); + if (prompt.getOptions() != null) { OpenAiChatOptions updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(prompt.getOptions(), ChatOptions.class, OpenAiChatOptions.class); - Set promptEnabledFunctions = this.handleFunctionCallbackConfigurations(updatedRuntimeOptions, - IS_RUNTIME_CALL); - functionsForThisRequest.addAll(promptEnabledFunctions); + enabledToolsToUse.addAll(this.runtimeFunctionCallbackConfigurations(updatedRuntimeOptions)); request = ModelOptionsUtils.merge(updatedRuntimeOptions, request, ChatCompletionRequest.class); } - if (this.defaultOptions != null) { - - Set defaultEnabledFunctions = this.handleFunctionCallbackConfigurations(this.defaultOptions, - !IS_RUNTIME_CALL); - - functionsForThisRequest.addAll(defaultEnabledFunctions); - - request = ModelOptionsUtils.merge(request, this.defaultOptions, ChatCompletionRequest.class); + if (!CollectionUtils.isEmpty(this.defaultOptions.getFunctions())) { + enabledToolsToUse.addAll(this.defaultOptions.getFunctions()); } + request = ModelOptionsUtils.merge(request, this.defaultOptions, ChatCompletionRequest.class); + // Add the enabled functions definitions to the request's tools parameter. - if (!CollectionUtils.isEmpty(functionsForThisRequest)) { + if (!CollectionUtils.isEmpty(enabledToolsToUse)) { request = ModelOptionsUtils.merge( - OpenAiChatOptions.builder().withTools(this.getFunctionTools(functionsForThisRequest)).build(), - request, ChatCompletionRequest.class); + OpenAiChatOptions.builder().withTools(this.getFunctionTools(enabledToolsToUse)).build(), request, + ChatCompletionRequest.class); } // Remove `streamOptions` from the request if it is not a streaming request diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/OpenAiTestConfiguration.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/OpenAiTestConfiguration.java index 5c3f80dbb..cbb88487f 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/OpenAiTestConfiguration.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/OpenAiTestConfiguration.java @@ -19,6 +19,7 @@ import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.ai.openai.api.OpenAiAudioApi; import org.springframework.ai.openai.api.OpenAiImageApi; +import org.springframework.ai.openai.api.OpenAiApi.ChatModel; import org.springframework.boot.SpringBootConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.util.StringUtils; @@ -52,7 +53,8 @@ public class OpenAiTestConfiguration { @Bean public OpenAiChatModel openAiChatModel(OpenAiApi api) { - OpenAiChatModel openAiChatModel = new OpenAiChatModel(api); + OpenAiChatModel openAiChatModel = new OpenAiChatModel(api, + OpenAiChatOptions.builder().withModel(ChatModel.GPT_4_O_MINI).build()); return openAiChatModel; } diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiPaymentTransactionIT.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiPaymentTransactionIT.java index dcb5e3fb4..66e91c0ed 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiPaymentTransactionIT.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiPaymentTransactionIT.java @@ -198,7 +198,7 @@ public class OpenAiPaymentTransactionIT { public OpenAiChatModel openAiClient(OpenAiApi openAiApi, FunctionCallbackContext functionCallbackContext) { return new OpenAiChatModel(openAiApi, OpenAiChatOptions.builder() - .withModel(ChatModel.GPT_4_TURBO.getName()) + .withModel(ChatModel.GPT_4_O_MINI.getName()) .withTemperature(0.1f) .build(), functionCallbackContext, RetryUtils.DEFAULT_RETRY_TEMPLATE); diff --git a/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatModel.java b/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatModel.java index f72177665..e02c4ad34 100644 --- a/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatModel.java +++ b/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatModel.java @@ -39,6 +39,7 @@ import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.model.ChatModelDescription; import org.springframework.ai.model.Media; import org.springframework.ai.model.ModelOptionsUtils; +import org.springframework.ai.model.function.FunctionCallback; import org.springframework.ai.model.function.FunctionCallbackContext; import org.springframework.ai.vertexai.gemini.metadata.VertexAiUsage; import org.springframework.beans.factory.DisposableBean; @@ -145,8 +146,13 @@ public class VertexAiGeminiChatModel extends AbstractToolCallSupport implements public VertexAiGeminiChatModel(VertexAI vertexAI, VertexAiGeminiChatOptions options, FunctionCallbackContext functionCallbackContext) { + this(vertexAI, options, functionCallbackContext, List.of()); + } - super(functionCallbackContext); + public VertexAiGeminiChatModel(VertexAI vertexAI, VertexAiGeminiChatOptions options, + FunctionCallbackContext functionCallbackContext, List toolFunctionCallbacks) { + + super(functionCallbackContext, options, toolFunctionCallbacks); Assert.notNull(vertexAI, "VertexAI must not be null"); Assert.notNull(options, "VertexAiGeminiChatOptions must not be null"); @@ -281,29 +287,22 @@ public class VertexAiGeminiChatModel extends AbstractToolCallSupport implements var generativeModelBuilder = new GenerativeModel.Builder().setModelName(this.defaultOptions.getModel()) .setVertexAi(this.vertexAI); - VertexAiGeminiChatOptions updatedRuntimeOptions = null; + VertexAiGeminiChatOptions updatedRuntimeOptions = VertexAiGeminiChatOptions.builder().build(); if (prompt.getOptions() != null) { updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(prompt.getOptions(), ChatOptions.class, VertexAiGeminiChatOptions.class); - functionsForThisRequest - .addAll(handleFunctionCallbackConfigurations(updatedRuntimeOptions, IS_RUNTIME_CALL)); + functionsForThisRequest.addAll(runtimeFunctionCallbackConfigurations(updatedRuntimeOptions)); } - if (this.defaultOptions != null) { - - functionsForThisRequest.addAll(handleFunctionCallbackConfigurations(this.defaultOptions, !IS_RUNTIME_CALL)); - - if (updatedRuntimeOptions == null) { - updatedRuntimeOptions = VertexAiGeminiChatOptions.builder().build(); - } - - updatedRuntimeOptions = ModelOptionsUtils.merge(updatedRuntimeOptions, this.defaultOptions, - VertexAiGeminiChatOptions.class); - + if (!CollectionUtils.isEmpty(this.defaultOptions.getFunctions())) { + functionsForThisRequest.addAll(this.defaultOptions.getFunctions()); } + updatedRuntimeOptions = ModelOptionsUtils.merge(updatedRuntimeOptions, this.defaultOptions, + VertexAiGeminiChatOptions.class); + if (updatedRuntimeOptions != null) { if (StringUtils.hasText(updatedRuntimeOptions.getModel()) diff --git a/spring-ai-core/src/main/java/org/springframework/ai/chat/model/AbstractToolCallSupport.java b/spring-ai-core/src/main/java/org/springframework/ai/chat/model/AbstractToolCallSupport.java index ba84329ab..608538564 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/chat/model/AbstractToolCallSupport.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/chat/model/AbstractToolCallSupport.java @@ -59,43 +59,72 @@ public abstract class AbstractToolCallSupport { protected final FunctionCallbackContext functionCallbackContext; protected AbstractToolCallSupport(FunctionCallbackContext functionCallbackContext) { + this(functionCallbackContext, FunctionCallingOptions.builder().build(), List.of()); + } + + protected AbstractToolCallSupport(FunctionCallbackContext functionCallbackContext, + FunctionCallingOptions functionCallingOptions, List toolFunctionCallbacks) { + this.functionCallbackContext = functionCallbackContext; + + List defaultFunctionCallbacks = merge(functionCallingOptions, toolFunctionCallbacks); + + if (!CollectionUtils.isEmpty(defaultFunctionCallbacks)) { + this.functionCallbackRegister.putAll(defaultFunctionCallbacks.stream() + .collect(ConcurrentHashMap::new, (m, v) -> m.put(v.getName(), v), ConcurrentHashMap::putAll)); + } + } + + private static List merge(FunctionCallingOptions funcitonOptions, + List toolFunctionCallbacks) { + List toolFunctionCallbacksCopy = new ArrayList<>(); + if (!CollectionUtils.isEmpty(toolFunctionCallbacks)) { + toolFunctionCallbacksCopy.addAll(toolFunctionCallbacks); + } + + if (!CollectionUtils.isEmpty(funcitonOptions.getFunctionCallbacks())) { + toolFunctionCallbacksCopy.addAll(funcitonOptions.getFunctionCallbacks()); + // Make sure that that function callbacks are are registered directly to the + // functionCallbackRegister and not passed in the default options. + funcitonOptions.setFunctionCallbacks(List.of()); + } + return toolFunctionCallbacksCopy; } public Map getFunctionCallbackRegister() { return this.functionCallbackRegister; } - protected Set handleFunctionCallbackConfigurations(FunctionCallingOptions options, boolean isRuntimeCall) { + /** + * Handle the runtime function callback configurations. Register the function + * callbacks + * @param runtimeFunctionOptions FunctionCallingOptions to handle. + * @return Set of function names to call. + */ + protected Set runtimeFunctionCallbackConfigurations(FunctionCallingOptions runtimeFunctionOptions) { - Set functionToCall = new HashSet<>(); + Set enabledFunctionsToCall = new HashSet<>(); - if (options != null) { - if (!CollectionUtils.isEmpty(options.getFunctionCallbacks())) { - options.getFunctionCallbacks().stream().forEach(functionCallback -> { - - // Register the tool callback. - if (isRuntimeCall) { - this.functionCallbackRegister.put(functionCallback.getName(), functionCallback); - } - else { - this.functionCallbackRegister.putIfAbsent(functionCallback.getName(), functionCallback); - } - - // Automatically enable the function, usually from prompt callback. - if (isRuntimeCall) { - functionToCall.add(functionCallback.getName()); - } - }); + if (runtimeFunctionOptions != null) { + // Add the explicitly enabled functions. + if (!CollectionUtils.isEmpty(runtimeFunctionOptions.getFunctions())) { + enabledFunctionsToCall.addAll(runtimeFunctionOptions.getFunctions()); } - // Add the explicitly enabled functions. - if (!CollectionUtils.isEmpty(options.getFunctions())) { - functionToCall.addAll(options.getFunctions()); + // Add the function callbacks to the register and automatically enable them. + if (!CollectionUtils.isEmpty(runtimeFunctionOptions.getFunctionCallbacks())) { + runtimeFunctionOptions.getFunctionCallbacks().stream().forEach(functionCallback -> { + + // Register the tool callback. + this.functionCallbackRegister.put(functionCallback.getName(), functionCallback); + + // Automatically enable the function, usually from prompt callback. + enabledFunctionsToCall.add(functionCallback.getName()); + }); } } - return functionToCall; + return enabledFunctionsToCall; } protected List handleToolCalls(Prompt prompt, ChatResponse response) { diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/anthropic/AnthropicAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/anthropic/AnthropicAutoConfiguration.java index 0d84ba76f..d782915e1 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/anthropic/AnthropicAutoConfiguration.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/anthropic/AnthropicAutoConfiguration.java @@ -33,7 +33,6 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.retry.support.RetryTemplate; -import org.springframework.util.CollectionUtils; import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClient; @@ -66,12 +65,8 @@ public class AnthropicAutoConfiguration { RetryTemplate retryTemplate, FunctionCallbackContext functionCallbackContext, List toolFunctionCallbacks) { - if (!CollectionUtils.isEmpty(toolFunctionCallbacks)) { - chatProperties.getOptions().getFunctionCallbacks().addAll(toolFunctionCallbacks); - } - - return new AnthropicChatModel(anthropicApi, chatProperties.getOptions(), retryTemplate, - functionCallbackContext); + return new AnthropicChatModel(anthropicApi, chatProperties.getOptions(), retryTemplate, functionCallbackContext, + toolFunctionCallbacks); } @Bean diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAiAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAiAutoConfiguration.java index 3c01a09ca..407efb82e 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAiAutoConfiguration.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAiAutoConfiguration.java @@ -15,12 +15,8 @@ */ package org.springframework.ai.autoconfigure.azure.openai; -import com.azure.ai.openai.OpenAIClient; -import com.azure.ai.openai.OpenAIClientBuilder; -import com.azure.core.credential.AzureKeyCredential; -import com.azure.core.credential.KeyCredential; -import com.azure.core.credential.TokenCredential; -import com.azure.core.util.ClientOptions; +import java.util.List; + import org.springframework.ai.azure.openai.AzureOpenAiAudioTranscriptionModel; import org.springframework.ai.azure.openai.AzureOpenAiChatModel; import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel; @@ -36,10 +32,14 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; -import java.util.List; +import com.azure.ai.openai.OpenAIClient; +import com.azure.ai.openai.OpenAIClientBuilder; +import com.azure.core.credential.AzureKeyCredential; +import com.azure.core.credential.KeyCredential; +import com.azure.core.credential.TokenCredential; +import com.azure.core.util.ClientOptions; /** * @author Piotr Olaszewski @@ -103,11 +103,8 @@ public class AzureOpenAiAutoConfiguration { AzureOpenAiChatProperties chatProperties, List toolFunctionCallbacks, FunctionCallbackContext functionCallbackContext) { - if (!CollectionUtils.isEmpty(toolFunctionCallbacks)) { - chatProperties.getOptions().getFunctionCallbacks().addAll(toolFunctionCallbacks); - } - - return new AzureOpenAiChatModel(openAIClient, chatProperties.getOptions(), functionCallbackContext); + return new AzureOpenAiChatModel(openAIClient, chatProperties.getOptions(), functionCallbackContext, + toolFunctionCallbacks); } @Bean diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/minimax/MiniMaxAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/minimax/MiniMaxAutoConfiguration.java index 80cde6dfe..d8d1f0a69 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/minimax/MiniMaxAutoConfiguration.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/minimax/MiniMaxAutoConfiguration.java @@ -37,6 +37,9 @@ import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClient; import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; /** * @author Geng Rong @@ -59,11 +62,16 @@ public class MiniMaxAutoConfiguration { var miniMaxApi = miniMaxApi(chatProperties.getBaseUrl(), commonProperties.getBaseUrl(), chatProperties.getApiKey(), commonProperties.getApiKey(), restClientBuilder, responseErrorHandler); + MiniMaxChatModel chatModel = new MiniMaxChatModel(miniMaxApi, chatProperties.getOptions(), + functionCallbackContext, retryTemplate); + if (!CollectionUtils.isEmpty(toolFunctionCallbacks)) { - chatProperties.getOptions().getFunctionCallbacks().addAll(toolFunctionCallbacks); + Map toolFunctionCallbackMap = toolFunctionCallbacks.stream() + .collect(Collectors.toMap(FunctionCallback::getName, Function.identity(), (a, b) -> b)); + chatModel.getFunctionCallbackRegister().putAll(toolFunctionCallbackMap); } - return new MiniMaxChatModel(miniMaxApi, chatProperties.getOptions(), functionCallbackContext, retryTemplate); + return chatModel; } @Bean diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mistralai/MistralAiAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mistralai/MistralAiAutoConfiguration.java index 0073311fe..0d5735f88 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mistralai/MistralAiAutoConfiguration.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mistralai/MistralAiAutoConfiguration.java @@ -35,7 +35,6 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.retry.support.RetryTemplate; import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClient; @@ -81,12 +80,8 @@ public class MistralAiAutoConfiguration { var mistralAiApi = mistralAiApi(chatProperties.getApiKey(), commonProperties.getApiKey(), chatProperties.getBaseUrl(), commonProperties.getBaseUrl(), restClientBuilder, responseErrorHandler); - if (!CollectionUtils.isEmpty(toolFunctionCallbacks)) { - chatProperties.getOptions().getFunctionCallbacks().addAll(toolFunctionCallbacks); - } - return new MistralAiChatModel(mistralAiApi, chatProperties.getOptions(), functionCallbackContext, - retryTemplate); + toolFunctionCallbacks, retryTemplate); } private MistralAiApi mistralAiApi(String apiKey, String commonApiKey, String baseUrl, String commonBaseUrl, diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/OllamaAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/OllamaAutoConfiguration.java index 87e8da125..a4a7f14d3 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/OllamaAutoConfiguration.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/OllamaAutoConfiguration.java @@ -32,7 +32,6 @@ import org.springframework.boot.autoconfigure.web.reactive.function.client.WebCl import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; -import org.springframework.util.CollectionUtils; import org.springframework.web.client.RestClient; /** @@ -68,11 +67,7 @@ public class OllamaAutoConfiguration { public OllamaChatModel ollamaChatModel(OllamaApi ollamaApi, OllamaChatProperties properties, List toolFunctionCallbacks, FunctionCallbackContext functionCallbackContext) { - if (!CollectionUtils.isEmpty(toolFunctionCallbacks)) { - properties.getOptions().getFunctionCallbacks().addAll(toolFunctionCallbacks); - } - - return new OllamaChatModel(ollamaApi, properties.getOptions(), functionCallbackContext); + return new OllamaChatModel(ollamaApi, properties.getOptions(), functionCallbackContext, toolFunctionCallbacks); } @Bean 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 baee94b0f..4ac30e0c6 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 @@ -41,7 +41,6 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.retry.support.RetryTemplate; import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClient; @@ -75,11 +74,8 @@ public class OpenAiAutoConfiguration { var openAiApi = openAiApi(chatProperties, commonProperties, restClientBuilder, webClientBuilder, responseErrorHandler, "chat"); - if (!CollectionUtils.isEmpty(toolFunctionCallbacks)) { - chatProperties.getOptions().getFunctionCallbacks().addAll(toolFunctionCallbacks); - } - - return new OpenAiChatModel(openAiApi, chatProperties.getOptions(), functionCallbackContext, retryTemplate); + return new OpenAiChatModel(openAiApi, chatProperties.getOptions(), functionCallbackContext, + toolFunctionCallbacks, retryTemplate); } @Bean diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/gemini/VertexAiGeminiAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/gemini/VertexAiGeminiAutoConfiguration.java index 69e048e7b..6003594f7 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/gemini/VertexAiGeminiAutoConfiguration.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/gemini/VertexAiGeminiAutoConfiguration.java @@ -18,10 +18,6 @@ package org.springframework.ai.autoconfigure.vertexai.gemini; import java.io.IOException; import java.util.List; -import com.google.auth.oauth2.GoogleCredentials; -import com.google.cloud.vertexai.VertexAI; - -import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiChatProperties; import org.springframework.ai.model.function.FunctionCallback; import org.springframework.ai.model.function.FunctionCallbackContext; import org.springframework.ai.model.function.FunctionCallbackWrapper.Builder.SchemaType; @@ -36,6 +32,9 @@ import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.cloud.vertexai.VertexAI; + /** * Auto-configuration for Vertex AI Gemini Chat. * @@ -84,11 +83,8 @@ public class VertexAiGeminiAutoConfiguration { FunctionCallbackContext functionCallbackContext = springAiFunctionManager(context); - if (!CollectionUtils.isEmpty(toolFunctionCallbacks)) { - chatProperties.getOptions().getFunctionCallbacks().addAll(toolFunctionCallbacks); - } - - return new VertexAiGeminiChatModel(vertexAi, chatProperties.getOptions(), functionCallbackContext); + return new VertexAiGeminiChatModel(vertexAi, chatProperties.getOptions(), functionCallbackContext, + toolFunctionCallbacks); } /** diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/zhipuai/ZhiPuAiAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/zhipuai/ZhiPuAiAutoConfiguration.java index 894b533b0..b49b70a14 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/zhipuai/ZhiPuAiAutoConfiguration.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/zhipuai/ZhiPuAiAutoConfiguration.java @@ -39,6 +39,9 @@ import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClient; import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; /** * @author Geng Rong @@ -61,11 +64,16 @@ public class ZhiPuAiAutoConfiguration { var zhiPuAiApi = zhiPuAiApi(chatProperties.getBaseUrl(), commonProperties.getBaseUrl(), chatProperties.getApiKey(), commonProperties.getApiKey(), restClientBuilder, responseErrorHandler); + ZhiPuAiChatModel chatModel = new ZhiPuAiChatModel(zhiPuAiApi, chatProperties.getOptions(), + functionCallbackContext, retryTemplate); + if (!CollectionUtils.isEmpty(toolFunctionCallbacks)) { - chatProperties.getOptions().getFunctionCallbacks().addAll(toolFunctionCallbacks); + Map toolFunctionCallbackMap = toolFunctionCallbacks.stream() + .collect(Collectors.toMap(FunctionCallback::getName, Function.identity(), (a, b) -> b)); + chatModel.getFunctionCallbackRegister().putAll(toolFunctionCallbackMap); } - return new ZhiPuAiChatModel(zhiPuAiApi, chatProperties.getOptions(), functionCallbackContext, retryTemplate); + return chatModel; } @Bean diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/tool/FunctionCallWithFunctionWrapperIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/tool/FunctionCallWithFunctionWrapperIT.java index 47ec9bfb0..9a54bef4e 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/tool/FunctionCallWithFunctionWrapperIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/tool/FunctionCallWithFunctionWrapperIT.java @@ -67,9 +67,7 @@ public class FunctionCallWithFunctionWrapperIT { logger.info("Response: {}", response); - assertThat(response.getResult().getOutput().getContent()).containsAnyOf("30.0", "30"); - assertThat(response.getResult().getOutput().getContent()).containsAnyOf("10.0", "10"); - assertThat(response.getResult().getOutput().getContent()).containsAnyOf("15", "15.0"); + assertThat(response.getResult().getOutput().getContent()).containsAnyOf("30", "10", "15"); }); } diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/OllamaChatAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/OllamaChatAutoConfigurationIT.java index 7ee593325..03e3a7145 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/OllamaChatAutoConfigurationIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/OllamaChatAutoConfigurationIT.java @@ -72,7 +72,7 @@ public class OllamaChatAutoConfigurationIT { createImage(ollamaContainer, OLLAMA_WITH_MODEL); } - static String baseUrl; + static String baseUrl = "http://localhost:11434"; @BeforeAll public static void beforeAll() throws IOException, InterruptedException { diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackInPrompt2IT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackInPrompt2IT.java index b3ff06447..cdf76ec24 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackInPrompt2IT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackInPrompt2IT.java @@ -27,6 +27,7 @@ import org.slf4j.LoggerFactory; import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.api.OpenAiApi.ChatModel; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -41,11 +42,12 @@ public class FunctionCallbackInPrompt2IT { @Test void functionCallTest() { - contextRunner.withPropertyValues("spring.ai.openai.chat.options.model=gpt-4o").run(context -> { + contextRunner.withPropertyValues("spring.ai.openai.chat.options.model=" + ChatModel.GPT_4_O_MINI.getName()) + .run(context -> { - OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); + OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); - ChatClient chatClient = ChatClient.builder(chatModel).build(); + ChatClient chatClient = ChatClient.builder(chatModel).build(); // @formatter:off chatClient.prompt() @@ -58,19 +60,18 @@ public class FunctionCallbackInPrompt2IT { .call().content(); // @formatter:on - logger.info("Response: {}", content); + logger.info("Response: {}", content); - assertThat(content).containsAnyOf("30.0", "30"); - assertThat(content).containsAnyOf("10.0", "10"); - assertThat(content).containsAnyOf("15.0", "15"); - }); + assertThat(content).contains("30", "10", "15"); + }); } @Test void functionCallTest2() { - contextRunner.withPropertyValues("spring.ai.openai.chat.options.model=gpt-4o").run(context -> { + contextRunner.withPropertyValues("spring.ai.openai.chat.options.model=" + ChatModel.GPT_4_O_MINI.getName()) + .run(context -> { - OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); + OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); // @formatter:off String content = ChatClient.builder(chatModel).build().prompt() @@ -84,18 +85,19 @@ public class FunctionCallbackInPrompt2IT { }) .call().content(); // @formatter:on - logger.info("Response: {}", content); + logger.info("Response: {}", content); - assertThat(content).contains("18"); - }); + assertThat(content).contains("18"); + }); } @Test void streamingFunctionCallTest() { - contextRunner.withPropertyValues("spring.ai.openai.chat.options.model=gpt-4o").run(context -> { + contextRunner.withPropertyValues("spring.ai.openai.chat.options.model=" + ChatModel.GPT_4_O_MINI.getName()) + .run(context -> { - OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); + OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); // @formatter:off String content = ChatClient.builder(chatModel).build().prompt() @@ -105,12 +107,10 @@ public class FunctionCallbackInPrompt2IT { .collectList().block().stream().collect(Collectors.joining()); // @formatter:on - logger.info("Response: {}", content); + logger.info("Response: {}", content); - assertThat(content).containsAnyOf("30.0", "30"); - assertThat(content).containsAnyOf("10.0", "10"); - assertThat(content).containsAnyOf("15.0", "15"); - }); + assertThat(content).contains("30", "10", "15"); + }); } } \ No newline at end of file diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackInPromptIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackInPromptIT.java index 9a087b93d..e5c0c4fca 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackInPromptIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackInPromptIT.java @@ -33,6 +33,7 @@ import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.model.function.FunctionCallbackWrapper; import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.ai.openai.OpenAiChatOptions; +import org.springframework.ai.openai.api.OpenAiApi.ChatModel; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -50,7 +51,7 @@ public class FunctionCallbackInPromptIT { @Test void functionCallTest() { contextRunner - .withPropertyValues("spring.ai.openai.chat.options.model=gpt-4o", + .withPropertyValues("spring.ai.openai.chat.options.model=" + ChatModel.GPT_4_O_MINI.getName(), "spring.ai.openai.chat.options.temperature=0.1") .run(context -> { @@ -79,7 +80,7 @@ public class FunctionCallbackInPromptIT { void streamingFunctionCallTest() { contextRunner - .withPropertyValues("spring.ai.openai.chat.options.model=gpt-4o", + .withPropertyValues("spring.ai.openai.chat.options.model=" + ChatModel.GPT_4_O_MINI.getName(), "spring.ai.openai.chat.options.temperature=0.5") .run(context -> { @@ -108,9 +109,7 @@ public class FunctionCallbackInPromptIT { .collect(Collectors.joining()); logger.info("Response: {}", content); - assertThat(content).containsAnyOf("30.0", "30"); - assertThat(content).containsAnyOf("10.0", "10"); - assertThat(content).containsAnyOf("15.0", "15"); + assertThat(content).contains("30", "10", "15"); }); } diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWithPlainFunctionBeanIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWithPlainFunctionBeanIT.java index e885f436b..ae29635fe 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWithPlainFunctionBeanIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWithPlainFunctionBeanIT.java @@ -34,6 +34,7 @@ import org.springframework.ai.chat.model.Generation; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.ai.openai.OpenAiChatOptions; +import org.springframework.ai.openai.api.OpenAiApi.ChatModel; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; @@ -54,36 +55,37 @@ class FunctionCallbackWithPlainFunctionBeanIT { @Test void functionCallTest() { - contextRunner.withPropertyValues("spring.ai.openai.chat.options.model=gpt-4o").run(context -> { + contextRunner.withPropertyValues("spring.ai.openai.chat.options.model=" + ChatModel.GPT_4_O_MINI.getName()) + .run(context -> { - OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); + OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); - // Test weatherFunction - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, Tokyo, and Paris? You can call the following functions 'weatherFunction'"); + // Test weatherFunction + UserMessage userMessage = new UserMessage( + "What's the weather like in San Francisco, Tokyo, and Paris? You can call the following functions 'weatherFunction'"); - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), - OpenAiChatOptions.builder().withFunction("weatherFunction").build())); + ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), + OpenAiChatOptions.builder().withFunction("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"); - // Test weatherFunctionTwo - response = chatModel.call(new Prompt(List.of(userMessage), - OpenAiChatOptions.builder().withFunction("weatherFunctionTwo").build())); + // Test weatherFunctionTwo + response = chatModel.call(new Prompt(List.of(userMessage), + OpenAiChatOptions.builder().withFunction("weatherFunctionTwo").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"); - }); + }); } @Test void functionCallWithPortableFunctionCallingOptions() { contextRunner - .withPropertyValues("spring.ai.openai.chat.options.model=gpt-4o", + .withPropertyValues("spring.ai.openai.chat.options.model=" + ChatModel.GPT_4_O_MINI.getName(), "spring.ai.openai.chat.options.temperature=0.1") .run(context -> { @@ -104,7 +106,7 @@ class FunctionCallbackWithPlainFunctionBeanIT { @Test void streamFunctionCallTest() { contextRunner - .withPropertyValues("spring.ai.openai.chat.options.model=gpt-4o", + .withPropertyValues("spring.ai.openai.chat.options.model=" + ChatModel.GPT_4_O_MINI.getName(), "spring.ai.openai.chat.options.temperature=0.1") .run(context -> { @@ -127,9 +129,7 @@ class FunctionCallbackWithPlainFunctionBeanIT { .collect(Collectors.joining()); logger.info("Response: {}", content); - assertThat(content).containsAnyOf("30.0", "30"); - assertThat(content).containsAnyOf("10.0", "10"); - assertThat(content).containsAnyOf("15.0", "15"); + assertThat(content).contains("30", "10", "15"); // Test weatherFunctionTwo response = chatModel.stream(new Prompt(List.of(userMessage), @@ -146,9 +146,8 @@ class FunctionCallbackWithPlainFunctionBeanIT { logger.info("Response: {}", content); assertThat(content).isNotEmpty().withFailMessage("Content returned from OpenAI model is empty"); - assertThat(content).containsAnyOf("30.0", "30"); - assertThat(content).containsAnyOf("10.0", "10"); - assertThat(content).containsAnyOf("15.0", "15"); + assertThat(content).contains("30", "10", "15"); + }); } diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWrapper2IT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWrapper2IT.java index e7ff2c15f..aaf84d98a 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWrapper2IT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWrapper2IT.java @@ -28,6 +28,7 @@ import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.model.function.FunctionCallback; import org.springframework.ai.model.function.FunctionCallbackWrapper; import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.api.OpenAiApi.ChatModel; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; @@ -39,7 +40,8 @@ public class FunctionCallbackWrapper2IT { private final Logger logger = LoggerFactory.getLogger(FunctionCallbackWrapperIT.class); private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY")) + .withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY"), + "spring.ai.openai.chat.options.model=" + ChatModel.GPT_4_O_MINI.getName()) .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) .withUserConfiguration(Config.class); @@ -62,9 +64,7 @@ public class FunctionCallbackWrapper2IT { logger.info("Response: {}", content); - assertThat(content).containsAnyOf("30.0", "30"); - assertThat(content).containsAnyOf("15.0", "15"); - assertThat(content).containsAnyOf("10", "10"); + assertThat(content).contains("30", "10", "15"); }); } @@ -84,9 +84,7 @@ public class FunctionCallbackWrapper2IT { logger.info("Response: {}", content); - assertThat(content).containsAnyOf("30.0", "30"); - assertThat(content).containsAnyOf("10.0", "10"); - assertThat(content).containsAnyOf("15.0", "15"); + assertThat(content).contains("30", "10", "15"); }); } diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWrapperIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWrapperIT.java index 722398a84..5020b4b56 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWrapperIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWrapperIT.java @@ -34,6 +34,7 @@ import org.springframework.ai.model.function.FunctionCallback; import org.springframework.ai.model.function.FunctionCallbackWrapper; import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.ai.openai.OpenAiChatOptions; +import org.springframework.ai.openai.api.OpenAiApi.ChatModel; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; @@ -47,7 +48,8 @@ public class FunctionCallbackWrapperIT { private final Logger logger = LoggerFactory.getLogger(FunctionCallbackWrapperIT.class); private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY")) + .withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY"), + "spring.ai.openai.chat.options.model=" + ChatModel.GPT_4_O_MINI.getName()) .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) .withUserConfiguration(Config.class); diff --git a/spring-ai-test/pom.xml b/spring-ai-test/pom.xml index 5c1f04db0..45cd0df17 100644 --- a/spring-ai-test/pom.xml +++ b/spring-ai-test/pom.xml @@ -43,6 +43,12 @@ org.springframework.boot spring-boot-starter-test + + + com.vaadin.external.google + android-json + +