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