Skip to content

Commit 401b696

Browse files
committed
ChatClientAutoConfiguration should back off if there are multiple ChatModels
Signed-off-by: Filip Hrisafov <[email protected]>
1 parent 0a1cf81 commit 401b696

File tree

13 files changed

+201
-16
lines changed

13 files changed

+201
-16
lines changed

auto-configurations/models/chat/client/spring-ai-autoconfigure-model-chat-client/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientAutoConfiguration.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
3737
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
3838
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
39+
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
3940
import org.springframework.boot.context.properties.EnableConfigurationProperties;
4041
import org.springframework.context.annotation.Bean;
4142
import org.springframework.context.annotation.Configuration;
@@ -82,6 +83,7 @@ ChatClientBuilderConfigurer chatClientBuilderConfigurer(ObjectProvider<ChatClien
8283
@Bean
8384
@Scope("prototype")
8485
@ConditionalOnMissingBean
86+
@ConditionalOnSingleCandidate(ChatModel.class)
8587
ChatClient.Builder chatClientBuilder(ChatClientBuilderConfigurer chatClientBuilderConfigurer, ChatModel chatModel,
8688
ObjectProvider<ObservationRegistry> observationRegistry,
8789
ObjectProvider<ChatClientObservationConvention> observationConvention) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/*
2+
* Copyright 2023-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.ai.model.chat.client.autoconfigure;
18+
19+
import java.util.List;
20+
21+
import org.junit.jupiter.api.Test;
22+
import org.junit.jupiter.api.extension.ExtendWith;
23+
import org.mockito.ArgumentCaptor;
24+
25+
import org.springframework.ai.chat.client.ChatClient;
26+
import org.springframework.ai.chat.client.ChatClientCustomizer;
27+
import org.springframework.ai.chat.messages.AssistantMessage;
28+
import org.springframework.ai.chat.messages.Message;
29+
import org.springframework.ai.chat.messages.MessageType;
30+
import org.springframework.ai.chat.model.ChatModel;
31+
import org.springframework.ai.chat.model.ChatResponse;
32+
import org.springframework.ai.chat.model.Generation;
33+
import org.springframework.ai.chat.prompt.Prompt;
34+
import org.springframework.ai.content.Content;
35+
import org.springframework.boot.autoconfigure.AutoConfigurations;
36+
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
37+
import org.springframework.boot.test.system.OutputCaptureExtension;
38+
import org.springframework.context.annotation.Bean;
39+
import org.springframework.context.annotation.Configuration;
40+
41+
import static org.assertj.core.api.Assertions.assertThat;
42+
import static org.assertj.core.api.Assertions.tuple;
43+
import static org.mockito.ArgumentMatchers.any;
44+
import static org.mockito.Mockito.mock;
45+
import static org.mockito.Mockito.verify;
46+
import static org.mockito.Mockito.when;
47+
48+
/**
49+
* @author Filip Hrisafov
50+
*/
51+
@ExtendWith(OutputCaptureExtension.class)
52+
class ChatClientAutoConfigurationTests {
53+
54+
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
55+
.withConfiguration(AutoConfigurations.of(ChatClientAutoConfiguration.class))
56+
.withUserConfiguration(MockConfig.class);
57+
58+
@Test
59+
void implicitlyEnabled() {
60+
this.contextRunner.run(context -> assertThat(context.getBeansOfType(ChatClient.Builder.class)).isNotEmpty());
61+
}
62+
63+
@Test
64+
void explicitlyEnabled() {
65+
this.contextRunner.withPropertyValues("spring.ai.chat.client.enabled=true")
66+
.run(context -> assertThat(context.getBeansOfType(ChatClient.Builder.class)).isNotEmpty());
67+
}
68+
69+
@Test
70+
void explicitlyDisabled() {
71+
this.contextRunner.withPropertyValues("spring.ai.chat.client.enabled=false")
72+
.run(context -> assertThat(context.getBeansOfType(ChatClient.Builder.class)).isEmpty());
73+
}
74+
75+
@Test
76+
void generate() {
77+
this.contextRunner.run(context -> {
78+
ChatClient.Builder builder = context.getBean(ChatClient.Builder.class);
79+
80+
assertThat(builder).isNotNull();
81+
82+
ChatClient chatClient = builder.build();
83+
ChatModel chatModel = context.getBean(ChatModel.class);
84+
85+
ChatResponse response = ChatResponse.builder()
86+
.generations(List.of(new Generation(new AssistantMessage("Test"))))
87+
.build();
88+
when(chatModel.call(any(Prompt.class))).thenReturn(response);
89+
90+
ChatResponse chatResponse = chatClient.prompt().user("Hello").call().chatResponse();
91+
assertThat(chatResponse).isSameAs(response);
92+
});
93+
}
94+
95+
@Test
96+
void testChatClientCustomizers() {
97+
this.contextRunner.withUserConfiguration(Config.class).run(context -> {
98+
99+
ChatClient.Builder builder = context.getBean(ChatClient.Builder.class);
100+
101+
ChatClient chatClient = builder.build();
102+
103+
assertThat(chatClient).isNotNull();
104+
105+
ChatModel chatModel = context.getBean(ChatModel.class);
106+
107+
ChatResponse response = ChatResponse.builder()
108+
.generations(List.of(new Generation(new AssistantMessage("Test"))))
109+
.build();
110+
when(chatModel.call(any(Prompt.class))).thenReturn(response);
111+
chatClient.prompt().user(u -> u.param("actor", "Tom Hanks")).call().chatResponse();
112+
113+
ArgumentCaptor<Prompt> promptArgument = ArgumentCaptor.forClass(Prompt.class);
114+
115+
verify(chatModel).call(promptArgument.capture());
116+
117+
Prompt prompt = promptArgument.getValue();
118+
assertThat(prompt.getInstructions()).extracting(Message::getMessageType, Content::getText)
119+
.containsExactly(tuple(MessageType.SYSTEM, "You are a movie expert."),
120+
tuple(MessageType.USER, "Generate the filmography of 5 movies for Tom Hanks."));
121+
});
122+
}
123+
124+
@Test
125+
void withMultipleChatModels() {
126+
this.contextRunner.withUserConfiguration(SecondChatModelConfig.class).run(context -> {
127+
assertThat(context).hasNotFailed();
128+
assertThat(context.getBeansOfType(ChatClient.Builder.class)).isEmpty();
129+
});
130+
}
131+
132+
record ActorsFilms(String actor, List<String> movies) {
133+
134+
}
135+
136+
@Configuration
137+
static class MockConfig {
138+
139+
@Bean
140+
ChatModel chatModel() {
141+
return mock(ChatModel.class);
142+
}
143+
144+
}
145+
146+
@Configuration
147+
static class SecondChatModelConfig {
148+
149+
@Bean
150+
ChatModel secondChatModel() {
151+
return mock(ChatModel.class);
152+
}
153+
154+
}
155+
156+
@Configuration
157+
static class Config {
158+
159+
@Bean
160+
public ChatClientCustomizer chatClientCustomizer() {
161+
return b -> b.defaultSystem("You are a movie expert.")
162+
.defaultUser("Generate the filmography of 5 movies for {actor}.");
163+
}
164+
165+
}
166+
167+
}

auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicChatAutoConfiguration.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,10 @@
5151
* @author Ilayaperumal Gopinathan
5252
* @since 1.0.0
5353
*/
54-
@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class,
55-
ToolCallingAutoConfiguration.class, WebClientAutoConfiguration.class })
54+
@AutoConfiguration(
55+
after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class,
56+
ToolCallingAutoConfiguration.class, WebClientAutoConfiguration.class },
57+
beforeName = { "org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfiguration" })
5658
@EnableConfigurationProperties({ AnthropicChatProperties.class, AnthropicConnectionProperties.class })
5759
@ConditionalOnClass(AnthropicApi.class)
5860
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.ANTHROPIC,

auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiChatAutoConfiguration.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@
4545
* @author Manuel Andreo Garcia
4646
* @author Ilayaperumal Gopinathan
4747
*/
48-
@AutoConfiguration(after = { ToolCallingAutoConfiguration.class })
48+
@AutoConfiguration(after = { ToolCallingAutoConfiguration.class },
49+
beforeName = { "org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfiguration" })
4950
@ConditionalOnClass({ AzureOpenAiChatModel.class })
5051
@EnableConfigurationProperties({ AzureOpenAiChatProperties.class })
5152
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.AZURE_OPENAI,

auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/converse/autoconfigure/BedrockConverseProxyChatAutoConfiguration.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@
5151
* @author Christian Tzolov
5252
* @author Wei Jiang
5353
*/
54-
@AutoConfiguration(after = { ToolCallingAutoConfiguration.class })
54+
@AutoConfiguration(after = { ToolCallingAutoConfiguration.class },
55+
beforeName = { "org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfiguration" })
5556
@EnableConfigurationProperties({ BedrockConverseProxyChatProperties.class, BedrockAwsConnectionConfiguration.class })
5657
@ConditionalOnClass({ BedrockProxyChatModel.class, BedrockRuntimeClient.class, BedrockRuntimeAsyncClient.class })
5758
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.BEDROCK_CONVERSE,

auto-configurations/models/spring-ai-autoconfigure-model-deepseek/src/main/java/org/springframework/ai/model/deepseek/autoconfigure/DeepSeekChatAutoConfiguration.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,10 @@
5151
*
5252
* @author Geng Rong
5353
*/
54-
@AutoConfiguration(after = { RestClientAutoConfiguration.class, WebClientAutoConfiguration.class,
55-
SpringAiRetryAutoConfiguration.class, ToolCallingAutoConfiguration.class })
54+
@AutoConfiguration(
55+
after = { RestClientAutoConfiguration.class, WebClientAutoConfiguration.class,
56+
SpringAiRetryAutoConfiguration.class, ToolCallingAutoConfiguration.class },
57+
beforeName = { "org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfiguration" })
5658
@ConditionalOnClass(DeepSeekApi.class)
5759
@EnableConfigurationProperties({ DeepSeekConnectionProperties.class, DeepSeekChatProperties.class })
5860
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.DEEPSEEK,

auto-configurations/models/spring-ai-autoconfigure-model-huggingface/src/main/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceChatAutoConfiguration.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
import org.springframework.boot.context.properties.EnableConfigurationProperties;
2727
import org.springframework.context.annotation.Bean;
2828

29-
@AutoConfiguration
29+
@AutoConfiguration(
30+
beforeName = { "org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfiguration" })
3031
@ConditionalOnClass(HuggingfaceChatModel.class)
3132
@EnableConfigurationProperties(HuggingfaceChatProperties.class)
3233
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.HUGGINGFACE,

auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxChatAutoConfiguration.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,10 @@
4949
* @author Geng Rong
5050
* @author Ilayaperumal Gopinathan
5151
*/
52-
@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class,
53-
ToolCallingAutoConfiguration.class })
52+
@AutoConfiguration(
53+
after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class,
54+
ToolCallingAutoConfiguration.class },
55+
beforeName = { "org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfiguration" })
5456
@ConditionalOnClass(MiniMaxApi.class)
5557
@EnableConfigurationProperties({ MiniMaxConnectionProperties.class, MiniMaxChatProperties.class })
5658
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.MINIMAX,

auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiChatAutoConfiguration.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,10 @@
5454
* @author Ilayaperumal Gopinathan
5555
* @since 0.8.1
5656
*/
57-
@AutoConfiguration(after = { RestClientAutoConfiguration.class, WebClientAutoConfiguration.class,
58-
SpringAiRetryAutoConfiguration.class, ToolCallingAutoConfiguration.class })
57+
@AutoConfiguration(
58+
after = { RestClientAutoConfiguration.class, WebClientAutoConfiguration.class,
59+
SpringAiRetryAutoConfiguration.class, ToolCallingAutoConfiguration.class },
60+
beforeName = { "org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfiguration" })
5961
@EnableConfigurationProperties({ MistralAiCommonProperties.class, MistralAiChatProperties.class })
6062
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.MISTRAL,
6163
matchIfMissing = true)

auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiChatAutoConfiguration.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@
3939
* @author Anders Swanson
4040
* @author Ilayaperumal Gopinathan
4141
*/
42-
@AutoConfiguration
42+
@AutoConfiguration(
43+
beforeName = { "org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfiguration" })
4344
@ConditionalOnClass(OCICohereChatModel.class)
4445
@EnableConfigurationProperties(OCICohereChatModelProperties.class)
4546
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.OCI_GENAI,

auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfiguration.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@
4949
* @author Ilayaperumal Gopinathan
5050
* @since 0.8.0
5151
*/
52-
@AutoConfiguration(after = { RestClientAutoConfiguration.class, ToolCallingAutoConfiguration.class })
52+
@AutoConfiguration(after = { RestClientAutoConfiguration.class, ToolCallingAutoConfiguration.class },
53+
beforeName = { "org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfiguration" })
5354
@ConditionalOnClass(OllamaChatModel.class)
5455
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.OLLAMA,
5556
matchIfMissing = true)

auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiChatAutoConfiguration.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@
5454
* @author Ilayaperumal Gopinathan
5555
* @since 1.0.0
5656
*/
57-
@AutoConfiguration(after = { SpringAiRetryAutoConfiguration.class, ToolCallingAutoConfiguration.class })
57+
@AutoConfiguration(after = { SpringAiRetryAutoConfiguration.class, ToolCallingAutoConfiguration.class },
58+
beforeName = { "org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfiguration" })
5859
@ConditionalOnClass({ VertexAI.class, VertexAiGeminiChatModel.class })
5960
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.VERTEX_AI,
6061
matchIfMissing = true)

auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiChatAutoConfiguration.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,10 @@
4949
* @author Geng Rong
5050
* @author Ilayaperumal Gopinathan
5151
*/
52-
@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class,
53-
ToolCallingAutoConfiguration.class })
52+
@AutoConfiguration(
53+
after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class,
54+
ToolCallingAutoConfiguration.class },
55+
beforeName = { "org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfiguration" })
5456
@ConditionalOnClass(ZhiPuAiApi.class)
5557
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.ZHIPUAI,
5658
matchIfMissing = true)

0 commit comments

Comments
 (0)