Add ChatClient.Builder auto-configuraiton support
- add ChatClinetCustomizer support. - add enable/disable property - enabled by default. - add ITs. * Update README.md
This commit is contained in:
committed by
Mark Pollack
parent
5f887b3eb6
commit
0571aac44c
98
README.md
98
README.md
@@ -28,19 +28,18 @@ Here is an example of existing code before the change
|
||||
|
||||
```java
|
||||
@RestController
|
||||
public class OldSimpleAiController {
|
||||
|
||||
private final ChatClient chatClient;
|
||||
|
||||
@Autowired
|
||||
public OldSimpleAiController(ChatClient chatClient) {
|
||||
this.chatClient = chatClient;
|
||||
}
|
||||
|
||||
@GetMapping("/ai/simple")
|
||||
public Map<String, String> completion(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
|
||||
return Map.of("generation", chatClient.call(message));
|
||||
}
|
||||
public class OldSimpleAiController {
|
||||
|
||||
private final ChatClient chatClient;
|
||||
|
||||
public OldSimpleAiController(ChatClient chatClient) {
|
||||
this.chatClient = chatClient;
|
||||
}
|
||||
|
||||
@GetMapping("/ai/simple")
|
||||
Map<String, String> completion(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
|
||||
return Map.of("generation", chatClient.call(message));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -49,18 +48,17 @@ Now after the changes this will be
|
||||
```java
|
||||
@RestController
|
||||
public class SimpleAiController {
|
||||
|
||||
private final ChatModel chatModel;
|
||||
|
||||
@Autowired
|
||||
public SimpleAiController(ChatModel chatModel) {
|
||||
this.chatModel = chatModel;
|
||||
}
|
||||
|
||||
@GetMapping("/ai/simple")
|
||||
public Map<String, String> completion(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
|
||||
return Map.of("generation", chatModel.call(message));
|
||||
}
|
||||
|
||||
private final ChatModel chatModel;
|
||||
|
||||
public SimpleAiController(ChatModel chatModel) {
|
||||
this.chatModel = chatModel;
|
||||
}
|
||||
|
||||
@GetMapping("/ai/simple")
|
||||
Map<String, String> completion(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
|
||||
return Map.of("generation", chatModel.call(message));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -79,17 +77,16 @@ Here is an example of existing code before the change
|
||||
|
||||
```java
|
||||
@RestController
|
||||
public class OldSimpleAiController {
|
||||
|
||||
private final ChatClient chatClient;
|
||||
|
||||
@Autowired
|
||||
public OldSimpleAiController(ChatClient chatClient) {
|
||||
this.chatClient = chatClient;
|
||||
class OldSimpleAiController {
|
||||
|
||||
ChatClient chatClient;
|
||||
|
||||
OldSimpleAiController(ChatClient chatClient) {
|
||||
this.chatClient = chatClient;
|
||||
}
|
||||
|
||||
@GetMapping("/ai/simple")
|
||||
public Map<String, String> completion(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
|
||||
Map<String, String> completion(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
|
||||
return Map.of(
|
||||
"generation",
|
||||
chatClient.call(message)
|
||||
@@ -103,37 +100,24 @@ Now after the changes this will be
|
||||
|
||||
```java
|
||||
@RestController
|
||||
public class SimpleAiController {
|
||||
class SimpleAiController {
|
||||
|
||||
private final ChatClient chatClient;
|
||||
|
||||
@Autowired
|
||||
public SimpleAiController(ChatClient chatClient) {
|
||||
this.chatClient = chatClient;
|
||||
}
|
||||
|
||||
@GetMapping("/ai/simple")
|
||||
public Map<String, String> completion(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
|
||||
return Map.of(
|
||||
private final ChatClient.Builder builder;
|
||||
|
||||
SimpleAiController(ChatClient.Builder builder) {
|
||||
this.builder = builder;
|
||||
}
|
||||
|
||||
@GetMapping("/ai/simple")
|
||||
Map<String, String> completion(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
|
||||
return Map.of(
|
||||
"generation",
|
||||
chatClient.prompt().user(message).call().content()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
and in your `@Configuration` class you need to define an `@Bean` as shown below
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
public class ApplicationConfiguration {
|
||||
|
||||
@Bean
|
||||
ChatClient chatClient(ChatModel chatModel) {
|
||||
return ChatClient.builder(chatModel).build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
NOTE: The `ChatModel` instance is made available to you through autoconfiguration.
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2024-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.chat.client;
|
||||
|
||||
import org.springframework.ai.chat.ChatClient;
|
||||
|
||||
/**
|
||||
* Callback interface that can be used to customize a {@link ChatClient.Builder
|
||||
* ChatClient.Builder}.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @author Mark Pollack
|
||||
* @author Josh Long
|
||||
* @author Arjen Poutsma
|
||||
* @since 1.0.0 M1
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ChatClientCustomizer {
|
||||
|
||||
/**
|
||||
* Callback to customize a {@link ChatClient.Builder ChatClient.Builder} instance.
|
||||
* @param chatClientBuilder the client builder to customize
|
||||
*/
|
||||
void customize(ChatClient.Builder chatClientBuilder);
|
||||
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.ai.chat;
|
||||
package org.springframework.ai.chat.client;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
@@ -29,6 +29,11 @@ import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.ai.chat.ChatClient;
|
||||
import org.springframework.ai.chat.ChatModel;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import org.springframework.ai.chat.Generation;
|
||||
import org.springframework.ai.chat.StreamingChatModel;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.messages.MessageType;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2024-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.autoconfigure.chat.client;
|
||||
|
||||
import org.springframework.ai.chat.ChatClient;
|
||||
import org.springframework.ai.chat.ChatModel;
|
||||
import org.springframework.ai.chat.client.ChatClientCustomizer;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for {@link ChatClient}.
|
||||
* <p>
|
||||
* This will produce a {@link ChatClient.Builder ChatClient.Builder} bean with the
|
||||
* {@code prototype} scope, meaning each injection point will receive a newly cloned
|
||||
* instance of the builder.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @author Mark Pollack
|
||||
* @author Josh Long
|
||||
* @author Arjen Poutsma
|
||||
* @since 1.0.0 M1
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@ConditionalOnClass(ChatClient.class)
|
||||
@EnableConfigurationProperties(ChatClientBuilderProperties.class)
|
||||
@ConditionalOnProperty(prefix = ChatClientBuilderProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
|
||||
matchIfMissing = true)
|
||||
public class ChatClientAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
ChatClientBuilderConfigurer chatClientBuilderConfigurer(ObjectProvider<ChatClientCustomizer> customizerProvider) {
|
||||
ChatClientBuilderConfigurer configurer = new ChatClientBuilderConfigurer();
|
||||
configurer.setChatClientCustomizers(customizerProvider.orderedStream().toList());
|
||||
return configurer;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Scope("prototype")
|
||||
@ConditionalOnMissingBean
|
||||
ChatClient.Builder chatClientBuilder(ChatClientBuilderConfigurer chatClientBuilderConfigurer, ChatModel chatModel) {
|
||||
ChatClient.Builder builder = ChatClient.builder(chatModel);
|
||||
return chatClientBuilderConfigurer.configure(builder);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2024-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.autoconfigure.chat.client;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.ai.chat.ChatClient;
|
||||
import org.springframework.ai.chat.client.ChatClientCustomizer;
|
||||
|
||||
/**
|
||||
* Builder for configuring a {@link ChatClient.Builder}.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @author Mark Pollack
|
||||
* @author Josh Long
|
||||
* @author Arjen Poutsma
|
||||
* @since 1.0.0 M1
|
||||
*/
|
||||
public class ChatClientBuilderConfigurer {
|
||||
|
||||
private List<ChatClientCustomizer> customizers;
|
||||
|
||||
void setChatClientCustomizers(List<ChatClientCustomizer> customizers) {
|
||||
this.customizers = customizers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the specified {@link ChatClient.Builder}. The builder can be further
|
||||
* tuned and default settings can be overridden.
|
||||
* @param builder the {@link ChatClient.Builder} instance to configure
|
||||
* @return the configured builder
|
||||
*/
|
||||
public ChatClient.Builder configure(ChatClient.Builder builder) {
|
||||
applyCustomizers(builder);
|
||||
return builder;
|
||||
}
|
||||
|
||||
private void applyCustomizers(ChatClient.Builder builder) {
|
||||
if (this.customizers != null) {
|
||||
for (ChatClientCustomizer customizer : this.customizers) {
|
||||
customizer.customize(builder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.autoconfigure.chat.client;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* Configuration properties for the chat client builder.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @author Mark Pollack
|
||||
* @author Josh Long
|
||||
* @author Arjen Poutsma
|
||||
* @since 1.0.0 M1
|
||||
*/
|
||||
@ConfigurationProperties(ChatClientBuilderProperties.CONFIG_PREFIX)
|
||||
public class ChatClientBuilderProperties {
|
||||
|
||||
public static final String CONFIG_PREFIX = "spring.ai.chat.client";
|
||||
|
||||
/**
|
||||
* Enable chat client builder.
|
||||
*/
|
||||
private boolean enabled = true;
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -33,3 +33,4 @@ org.springframework.ai.autoconfigure.watsonxai.WatsonxAiAutoConfiguration
|
||||
org.springframework.ai.autoconfigure.vectorstore.elasticsearch.ElasticsearchVectorStoreAutoConfiguration
|
||||
org.springframework.ai.autoconfigure.vectorstore.cassandra.CassandraVectorStoreAutoConfiguration
|
||||
org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiAutoConfiguration
|
||||
org.springframework.ai.autoconfigure.chat.client.ChatClientAutoConfiguration
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright 2024-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.autoconfigure.chat.client;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
|
||||
import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration;
|
||||
import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration;
|
||||
import org.springframework.ai.chat.ChatClient;
|
||||
import org.springframework.ai.chat.client.ChatClientCustomizer;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*")
|
||||
public class ChatClientAutoConfigurationIT {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(ChatClientAutoConfigurationIT.class);
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY"))
|
||||
.withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
|
||||
RestClientAutoConfiguration.class, OpenAiAutoConfiguration.class, ChatClientAutoConfiguration.class));
|
||||
|
||||
@Test
|
||||
void implicitlyEnabled() {
|
||||
contextRunner.run(context -> {
|
||||
assertThat(context.getBeansOfType(ChatClient.Builder.class)).isNotEmpty();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void explicitlyEnabled() {
|
||||
contextRunner.withPropertyValues("spring.ai.chat.client.enabled=true").run(context -> {
|
||||
assertThat(context.getBeansOfType(ChatClient.Builder.class)).isNotEmpty();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void explicitlyDisabled() {
|
||||
contextRunner.withPropertyValues("spring.ai.chat.client.enabled=false").run(context -> {
|
||||
assertThat(context.getBeansOfType(ChatClient.Builder.class)).isEmpty();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void generate() {
|
||||
contextRunner.run(context -> {
|
||||
ChatClient.Builder builder = context.getBean(ChatClient.Builder.class);
|
||||
|
||||
assertThat(builder).isNotNull();
|
||||
|
||||
ChatClient chatClient = builder.build();
|
||||
|
||||
String response = chatClient.prompt().user("Hello").call().content();
|
||||
|
||||
assertThat(response).isNotEmpty();
|
||||
logger.info("Response: " + response);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testChatClientCustomizers() {
|
||||
contextRunner.withUserConfiguration(Config.class).run(context -> {
|
||||
|
||||
ChatClient.Builder builder = context.getBean(ChatClient.Builder.class);
|
||||
|
||||
ChatClient chatClient = builder.build();
|
||||
|
||||
assertThat(chatClient).isNotNull();
|
||||
|
||||
ActorsFilms actorsFilms = chatClient.prompt()
|
||||
.user(u -> u.param("actor", "Tom Hanks"))
|
||||
.call()
|
||||
.entity(ActorsFilms.class);
|
||||
|
||||
logger.info("" + actorsFilms);
|
||||
assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
|
||||
assertThat(actorsFilms.movies()).hasSize(5);
|
||||
});
|
||||
}
|
||||
|
||||
record ActorsFilms(String actor, List<String> movies) {
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
public ChatClientCustomizer chatClientCustomizer() {
|
||||
return b -> b.defaultSystem("You are a movie expert.")
|
||||
.defaultUser("Generate the filmography of 5 movies for {actor}.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user