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
This commit is contained in:
@@ -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())
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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<String> 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<String> 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");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -533,7 +533,7 @@ public class DefaultChatClient implements ChatClient {
|
||||
List<RequestResponseAdvisor> advisors, Map<String, Object> 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);
|
||||
|
||||
@@ -28,4 +28,6 @@ public interface ChatOptions extends ModelOptions {
|
||||
|
||||
Integer getTopK();
|
||||
|
||||
ChatOptions copy();
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user