From 018bb2c60056c323f0119d12bae2f90d942d59d1 Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Wed, 17 Jul 2024 17:55:19 +0200 Subject: [PATCH] Ensure that ChatClient copies the input chatoptions - introduce copy() method to the ChatOptions. - make sure that the DefaultChatClientRequestSpec takes a copy of the input chat options to prevent multation. - add a OpenAiChatClientMultipleFunctionCallsIT to reproduce the problem and verify the solution. Resolves #1064 --- .../ai/anthropic/AnthropicChatOptions.java | 5 + .../azure/openai/AzureOpenAiChatOptions.java | 5 + .../anthropic/AnthropicChatOptions.java | 5 + .../anthropic3/Anthropic3ChatOptions.java | 5 + .../cohere/BedrockCohereChatOptions.java | 5 + .../BedrockAi21Jurassic2ChatOptions.java | 5 + .../llama/BedrockLlamaChatOptions.java | 5 + .../titan/BedrockTitanChatOptions.java | 5 + .../ai/minimax/MiniMaxChatOptions.java | 5 + .../ai/mistralai/MistralAiChatOptions.java | 5 + .../ai/moonshot/MoonshotChatOptions.java | 18 +++ .../ai/ollama/api/OllamaOptions.java | 5 + .../ai/openai/OpenAiChatOptions.java | 5 + ...enAiChatClientMultipleFunctionCallsIT.java | 126 ++++++++++++++++++ .../ai/qianfan/QianFanChatOptions.java | 5 + .../gemini/VertexAiGeminiChatOptions.java | 5 + .../palm2/VertexAiPaLm2ChatOptions.java | 5 + .../ai/watsonx/WatsonxAiChatOptions.java | 5 + .../ai/zhipuai/ZhiPuAiChatOptions.java | 5 + .../ai/chat/client/ChatClient.java | 6 +- .../ai/chat/client/DefaultChatClient.java | 2 +- .../ai/chat/prompt/ChatOptions.java | 2 + .../ai/chat/prompt/ChatOptionsBuilder.java | 5 + .../FunctionCallingOptionsBuilder.java | 10 ++ 24 files changed, 249 insertions(+), 5 deletions(-) create mode 100644 models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientMultipleFunctionCallsIT.java diff --git a/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/AnthropicChatOptions.java b/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/AnthropicChatOptions.java index f971b75d9..9bc3fdabe 100644 --- a/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/AnthropicChatOptions.java +++ b/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/AnthropicChatOptions.java @@ -229,6 +229,11 @@ public class AnthropicChatOptions implements ChatOptions, FunctionCallingOptions this.functions = functions; } + @Override + public AnthropicChatOptions copy() { + return fromOptions(this); + } + public static AnthropicChatOptions fromOptions(AnthropicChatOptions fromOptions) { return builder().withModel(fromOptions.getModel()) .withMaxTokens(fromOptions.getMaxTokens()) diff --git a/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiChatOptions.java b/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiChatOptions.java index d701a4638..07ac7236b 100644 --- a/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiChatOptions.java +++ b/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiChatOptions.java @@ -378,6 +378,11 @@ public class AzureOpenAiChatOptions implements FunctionCallingOptions, ChatOptio this.responseFormat = responseFormat; } + @Override + public AzureOpenAiChatOptions copy() { + return fromOptions(this); + } + public static AzureOpenAiChatOptions fromOptions(AzureOpenAiChatOptions fromOptions) { return builder().withDeploymentName(fromOptions.getDeploymentName()) .withFrequencyPenalty( diff --git a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/anthropic/AnthropicChatOptions.java b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/anthropic/AnthropicChatOptions.java index d0d5a5a2c..55b37032f 100644 --- a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/anthropic/AnthropicChatOptions.java +++ b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/anthropic/AnthropicChatOptions.java @@ -164,6 +164,11 @@ public class AnthropicChatOptions implements ChatOptions { this.anthropicVersion = anthropicVersion; } + @Override + public AnthropicChatOptions copy() { + return fromOptions(this); + } + public static AnthropicChatOptions fromOptions(AnthropicChatOptions fromOptions) { return builder().withTemperature(fromOptions.getTemperature()) .withMaxTokensToSample(fromOptions.getMaxTokensToSample()) diff --git a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/anthropic3/Anthropic3ChatOptions.java b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/anthropic3/Anthropic3ChatOptions.java index b4995683a..0f44c212b 100644 --- a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/anthropic3/Anthropic3ChatOptions.java +++ b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/anthropic3/Anthropic3ChatOptions.java @@ -163,6 +163,11 @@ public class Anthropic3ChatOptions implements ChatOptions { this.anthropicVersion = anthropicVersion; } + @Override + public Anthropic3ChatOptions copy() { + return fromOptions(this); + } + public static Anthropic3ChatOptions fromOptions(Anthropic3ChatOptions fromOptions) { return builder().withTemperature(fromOptions.getTemperature()) .withMaxTokens(fromOptions.getMaxTokens()) diff --git a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/cohere/BedrockCohereChatOptions.java b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/cohere/BedrockCohereChatOptions.java index e0ab181cc..619aa8d7b 100644 --- a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/cohere/BedrockCohereChatOptions.java +++ b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/cohere/BedrockCohereChatOptions.java @@ -213,6 +213,11 @@ public class BedrockCohereChatOptions implements ChatOptions { this.truncate = truncate; } + @Override + public BedrockCohereChatOptions copy() { + return fromOptions(this); + } + public static BedrockCohereChatOptions fromOptions(BedrockCohereChatOptions fromOptions) { return builder().withTemperature(fromOptions.getTemperature()) .withTopP(fromOptions.getTopP()) diff --git a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/jurassic2/BedrockAi21Jurassic2ChatOptions.java b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/jurassic2/BedrockAi21Jurassic2ChatOptions.java index c165c61c1..723f427ca 100644 --- a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/jurassic2/BedrockAi21Jurassic2ChatOptions.java +++ b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/jurassic2/BedrockAi21Jurassic2ChatOptions.java @@ -413,6 +413,11 @@ public class BedrockAi21Jurassic2ChatOptions implements ChatOptions { } } + @Override + public BedrockAi21Jurassic2ChatOptions copy() { + return fromOptions(this); + } + public static BedrockAi21Jurassic2ChatOptions fromOptions(BedrockAi21Jurassic2ChatOptions fromOptions) { return builder().withPrompt(fromOptions.getPrompt()) .withNumResults(fromOptions.getNumResults()) diff --git a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/llama/BedrockLlamaChatOptions.java b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/llama/BedrockLlamaChatOptions.java index 4d6c0a6e0..cf0e68ea7 100644 --- a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/llama/BedrockLlamaChatOptions.java +++ b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/llama/BedrockLlamaChatOptions.java @@ -109,6 +109,11 @@ public class BedrockLlamaChatOptions implements ChatOptions { throw new UnsupportedOperationException("Unsupported option: 'TopK'"); } + @Override + public BedrockLlamaChatOptions copy() { + return fromOptions(this); + } + public static BedrockLlamaChatOptions fromOptions(BedrockLlamaChatOptions fromOptions) { return builder().withTemperature(fromOptions.getTemperature()) .withTopP(fromOptions.getTopP()) diff --git a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/titan/BedrockTitanChatOptions.java b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/titan/BedrockTitanChatOptions.java index d53126a0b..364cd1bc4 100644 --- a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/titan/BedrockTitanChatOptions.java +++ b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/titan/BedrockTitanChatOptions.java @@ -128,6 +128,11 @@ public class BedrockTitanChatOptions implements ChatOptions { throw new UnsupportedOperationException("Bedrock Titan Chat does not support the 'TopK' option.'"); } + @Override + public BedrockTitanChatOptions copy() { + return fromOptions(this); + } + public static BedrockTitanChatOptions fromOptions(BedrockTitanChatOptions fromOptions) { return builder().withTemperature(fromOptions.getTemperature()) .withTopP(fromOptions.getTopP()) diff --git a/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/MiniMaxChatOptions.java b/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/MiniMaxChatOptions.java index 10ed0f51d..6a6bfc999 100644 --- a/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/MiniMaxChatOptions.java +++ b/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/MiniMaxChatOptions.java @@ -467,6 +467,11 @@ public class MiniMaxChatOptions implements FunctionCallingOptions, ChatOptions { return true; } + @Override + public MiniMaxChatOptions copy() { + return fromOptions(this); + } + public static MiniMaxChatOptions fromOptions(MiniMaxChatOptions fromOptions) { return builder().withModel(fromOptions.getModel()) .withFrequencyPenalty(fromOptions.getFrequencyPenalty()) diff --git a/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/MistralAiChatOptions.java b/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/MistralAiChatOptions.java index 10bda6c95..cadb5e8f9 100644 --- a/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/MistralAiChatOptions.java +++ b/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/MistralAiChatOptions.java @@ -315,6 +315,11 @@ public class MistralAiChatOptions implements FunctionCallingOptions, ChatOptions this.functions = functions; } + @Override + public MistralAiChatOptions copy() { + return fromOptions(this); + } + public static MistralAiChatOptions fromOptions(MistralAiChatOptions fromOptions) { return builder().withModel(fromOptions.getModel()) .withMaxTokens(fromOptions.getMaxTokens()) diff --git a/models/spring-ai-moonshot/src/main/java/org/springframework/ai/moonshot/MoonshotChatOptions.java b/models/spring-ai-moonshot/src/main/java/org/springframework/ai/moonshot/MoonshotChatOptions.java index 8a489fcb9..739e34d1a 100644 --- a/models/spring-ai-moonshot/src/main/java/org/springframework/ai/moonshot/MoonshotChatOptions.java +++ b/models/spring-ai-moonshot/src/main/java/org/springframework/ai/moonshot/MoonshotChatOptions.java @@ -303,6 +303,24 @@ public class MoonshotChatOptions implements ChatOptions { this.user = user; } + @Override + public MoonshotChatOptions copy() { + return builder().withModel(this.model) + .withMaxTokens(this.maxTokens) + .withTemperature(this.temperature) + .withTopP(this.topP) + .withN(this.n) + .withPresencePenalty(this.presencePenalty) + .withFrequencyPenalty(this.frequencyPenalty) + .withStop(this.stop) + .withUser(this.user) + .withTools(this.tools) + .withToolChoice(this.toolChoice) + .withFunctionCallbacks(this.functionCallbacks) + .withFunctions(this.functions) + .build(); + } + @Override public int hashCode() { final int prime = 31; diff --git a/models/spring-ai-ollama/src/main/java/org/springframework/ai/ollama/api/OllamaOptions.java b/models/spring-ai-ollama/src/main/java/org/springframework/ai/ollama/api/OllamaOptions.java index 436631f6e..5847bc82c 100644 --- a/models/spring-ai-ollama/src/main/java/org/springframework/ai/ollama/api/OllamaOptions.java +++ b/models/spring-ai-ollama/src/main/java/org/springframework/ai/ollama/api/OllamaOptions.java @@ -714,6 +714,11 @@ public class OllamaOptions implements ChatOptions, EmbeddingOptions { .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } + @Override + public OllamaOptions copy() { + return fromOptions(this); + } + public static OllamaOptions fromOptions(OllamaOptions fromOptions) { return new OllamaOptions() .withModel(fromOptions.getModel()) diff --git a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatOptions.java b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatOptions.java index 40e61bdce..e5738e41f 100644 --- a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatOptions.java +++ b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatOptions.java @@ -609,6 +609,11 @@ public class OpenAiChatOptions implements FunctionCallingOptions, ChatOptions { throw new UnsupportedOperationException("Unimplemented method 'setTopK'"); } + @Override + public OpenAiChatOptions copy() { + return OpenAiChatOptions.fromOptions(this); + } + public static OpenAiChatOptions fromOptions(OpenAiChatOptions fromOptions) { return OpenAiChatOptions.builder() .withModel(fromOptions.getModel()) diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientMultipleFunctionCallsIT.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientMultipleFunctionCallsIT.java new file mode 100644 index 000000000..da045d0bb --- /dev/null +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientMultipleFunctionCallsIT.java @@ -0,0 +1,126 @@ +/* + * Copyright 2023 - 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.ai.openai.chat.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.openai.OpenAiTestConfiguration; +import org.springframework.ai.openai.api.tool.MockWeatherService; +import org.springframework.ai.openai.testutils.AbstractIT; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.io.Resource; +import org.springframework.test.context.ActiveProfiles; + +import reactor.core.publisher.Flux; + +@SpringBootTest(classes = OpenAiTestConfiguration.class) +@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+") +@ActiveProfiles("logging-test") +class OpenAiChatClientMultipleFunctionCallsIT extends AbstractIT { + + private static final Logger logger = LoggerFactory.getLogger(OpenAiChatClientMultipleFunctionCallsIT.class); + + @Value("classpath:/prompts/system-message.st") + private Resource systemTextResource; + + record ActorsFilms(String actor, List movies) { + } + + @Test + void turnFunctionsOnAndOffTest() { + + var chatClientBuilder = ChatClient.builder(chatModel); + + // @formatter:off + String response = chatClientBuilder.build().prompt() + .user(u -> u.text("What's the weather like in San Francisco, Tokyo, and Paris?")) + .call() + .content(); + // @formatter:on + + logger.info("Response: {}", response); + + assertThat(response).doesNotContain("30", "10", "15"); + + // @formatter:off + response = chatClientBuilder.build().prompt() + .user(u -> u.text("What's the weather like in San Francisco, Tokyo, and Paris?")) + .function("getCurrentWeather", "Get the weather in location", new MockWeatherService()) + .call() + .content(); + // @formatter:on + + logger.info("Response: {}", response); + + assertThat(response).contains("30", "10", "15"); + + // @formatter:off + response = chatClientBuilder.build().prompt() + .user(u -> u.text("What's the weather like in San Francisco, Tokyo, and Paris?")) + .call() + .content(); + // @formatter:on + + logger.info("Response: {}", response); + + assertThat(response).doesNotContain("30", "10", "15"); + + } + + @Test + void defaultFunctionCallTest() { + + // @formatter:off + String response = ChatClient.builder(chatModel) + .defaultFunction("getCurrentWeather", "Get the weather in location", new MockWeatherService()) + .defaultUser(u -> u.text("What's the weather like in San Francisco, Tokyo, and Paris?")) + .build() + .prompt().call().content(); + // @formatter:on + + logger.info("Response: {}", response); + + assertThat(response).contains("30", "10", "15"); + } + + @Test + void streamFunctionCallTest() { + + // @formatter:off + Flux response = ChatClient.create(chatModel).prompt() + .user("What's the weather like in San Francisco, Tokyo, and Paris?") + .function("getCurrentWeather", "Get the weather in location", new MockWeatherService()) + .stream() + .content(); + // @formatter:on + + String content = response.collectList().block().stream().collect(Collectors.joining()); + logger.info("Response: {}", content); + + assertThat(content).contains("30", "10", "15"); + + } + +} \ No newline at end of file diff --git a/models/spring-ai-qianfan/src/main/java/org/springframework/ai/qianfan/QianFanChatOptions.java b/models/spring-ai-qianfan/src/main/java/org/springframework/ai/qianfan/QianFanChatOptions.java index bc14bb252..e87047429 100644 --- a/models/spring-ai-qianfan/src/main/java/org/springframework/ai/qianfan/QianFanChatOptions.java +++ b/models/spring-ai-qianfan/src/main/java/org/springframework/ai/qianfan/QianFanChatOptions.java @@ -290,6 +290,11 @@ public class QianFanChatOptions implements ChatOptions { return true; } + @Override + public QianFanChatOptions copy() { + return fromOptions(this); + } + public static QianFanChatOptions fromOptions(QianFanChatOptions fromOptions) { return QianFanChatOptions.builder() .withModel(fromOptions.getModel()) diff --git a/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatOptions.java b/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatOptions.java index c19c25633..ec4258482 100644 --- a/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatOptions.java +++ b/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatOptions.java @@ -349,6 +349,11 @@ public class VertexAiGeminiChatOptions implements FunctionCallingOptions, ChatOp + super.toString() + "]"; } + @Override + public VertexAiGeminiChatOptions copy() { + return fromOptions(this); + } + public static VertexAiGeminiChatOptions fromOptions(VertexAiGeminiChatOptions fromOptions) { VertexAiGeminiChatOptions options = new VertexAiGeminiChatOptions(); options.setStopSequences(fromOptions.getStopSequences()); diff --git a/models/spring-ai-vertex-ai-palm2/src/main/java/org/springframework/ai/vertexai/palm2/VertexAiPaLm2ChatOptions.java b/models/spring-ai-vertex-ai-palm2/src/main/java/org/springframework/ai/vertexai/palm2/VertexAiPaLm2ChatOptions.java index 55742ff05..06f4cfa8b 100644 --- a/models/spring-ai-vertex-ai-palm2/src/main/java/org/springframework/ai/vertexai/palm2/VertexAiPaLm2ChatOptions.java +++ b/models/spring-ai-vertex-ai-palm2/src/main/java/org/springframework/ai/vertexai/palm2/VertexAiPaLm2ChatOptions.java @@ -127,6 +127,11 @@ public class VertexAiPaLm2ChatOptions implements ChatOptions { this.topK = topK; } + @Override + public VertexAiPaLm2ChatOptions copy() { + return fromOptions(this); + } + public static VertexAiPaLm2ChatOptions fromOptions(VertexAiPaLm2ChatOptions fromOptions) { return VertexAiPaLm2ChatOptions.builder() .withTemperature(fromOptions.getTemperature()) diff --git a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiChatOptions.java b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiChatOptions.java index 28bd02159..ba87789ff 100644 --- a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiChatOptions.java +++ b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiChatOptions.java @@ -324,6 +324,11 @@ public class WatsonxAiChatOptions implements ChatOptions { return input != null ? input.replaceAll("([a-z])([A-Z]+)", "$1_$2").toLowerCase() : null; } + @Override + public WatsonxAiChatOptions copy() { + return fromOptions(this); + } + public static WatsonxAiChatOptions fromOptions(WatsonxAiChatOptions fromOptions) { return WatsonxAiChatOptions.builder() .withTemperature(fromOptions.getTemperature()) diff --git a/models/spring-ai-zhipuai/src/main/java/org/springframework/ai/zhipuai/ZhiPuAiChatOptions.java b/models/spring-ai-zhipuai/src/main/java/org/springframework/ai/zhipuai/ZhiPuAiChatOptions.java index f74074163..7a9c3968f 100644 --- a/models/spring-ai-zhipuai/src/main/java/org/springframework/ai/zhipuai/ZhiPuAiChatOptions.java +++ b/models/spring-ai-zhipuai/src/main/java/org/springframework/ai/zhipuai/ZhiPuAiChatOptions.java @@ -412,6 +412,11 @@ public class ZhiPuAiChatOptions implements FunctionCallingOptions, ChatOptions { throw new UnsupportedOperationException("Unimplemented method 'setTopK'"); } + @Override + public ZhiPuAiChatOptions copy() { + return fromOptions(this); + } + public static ZhiPuAiChatOptions fromOptions(ZhiPuAiChatOptions fromOptions) { return ZhiPuAiChatOptions.builder() .withModel(fromOptions.getModel()) diff --git a/spring-ai-core/src/main/java/org/springframework/ai/chat/client/ChatClient.java b/spring-ai-core/src/main/java/org/springframework/ai/chat/client/ChatClient.java index 34d42a8aa..12feb62b1 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/chat/client/ChatClient.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/chat/client/ChatClient.java @@ -17,16 +17,12 @@ package org.springframework.ai.chat.client; import java.net.URL; import java.nio.charset.Charset; -import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.function.Consumer; -import reactor.core.publisher.Flux; - import org.springframework.ai.chat.messages.Media; import org.springframework.ai.chat.messages.Message; -import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.ChatOptions; @@ -36,6 +32,8 @@ import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.io.Resource; import org.springframework.util.MimeType; +import reactor.core.publisher.Flux; + /** * Client to perform stateless requests to an AI Model, using a fluent API. * diff --git a/spring-ai-core/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.java b/spring-ai-core/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.java index a54d11fe4..78f1341ac 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.java @@ -533,7 +533,7 @@ public class DefaultChatClient implements ChatClient { List advisors, Map advisorParams) { this.chatModel = chatModel; - this.chatOptions = chatOptions != null ? chatOptions : chatModel.getDefaultOptions(); + this.chatOptions = chatOptions != null ? chatOptions.copy() : chatModel.getDefaultOptions().copy(); this.userText = userText; this.userParams.putAll(userParams); diff --git a/spring-ai-core/src/main/java/org/springframework/ai/chat/prompt/ChatOptions.java b/spring-ai-core/src/main/java/org/springframework/ai/chat/prompt/ChatOptions.java index 38b9cd0f6..24545ad3c 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/chat/prompt/ChatOptions.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/chat/prompt/ChatOptions.java @@ -28,4 +28,6 @@ public interface ChatOptions extends ModelOptions { Integer getTopK(); + ChatOptions copy(); + } diff --git a/spring-ai-core/src/main/java/org/springframework/ai/chat/prompt/ChatOptionsBuilder.java b/spring-ai-core/src/main/java/org/springframework/ai/chat/prompt/ChatOptionsBuilder.java index d367af422..3b99d3a6e 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/chat/prompt/ChatOptionsBuilder.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/chat/prompt/ChatOptionsBuilder.java @@ -52,6 +52,11 @@ public class ChatOptionsBuilder { this.topK = topK; } + @Override + public ChatOptions copy() { + return builder().withTemperature(this.temperature).withTopP(this.topP).withTopK(this.topK).build(); + } + } private final ChatOptionsImpl options = new ChatOptionsImpl(); diff --git a/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallingOptionsBuilder.java b/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallingOptionsBuilder.java index 8b30da67d..ccdc8feee 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallingOptionsBuilder.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallingOptionsBuilder.java @@ -139,6 +139,16 @@ public class FunctionCallingOptionsBuilder { this.topK = topK; } + @Override + public ChatOptions copy() { + return new FunctionCallingOptionsBuilder().withTemperature(this.temperature) + .withTopP(this.topP) + .withTopK(this.topK) + .withFunctions(this.functions) + .withFunctionCallbacks(this.functionCallbacks) + .build(); + } + } } \ No newline at end of file