Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ChatClient.Builder auto-configuraiton support #760

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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);

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -29,6 +29,11 @@
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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}

}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}

}
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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}.");
}

}

}