diff --git a/.sdkmanrc b/.sdkmanrc
index 45ff88f..6665484 100644
--- a/.sdkmanrc
+++ b/.sdkmanrc
@@ -3,4 +3,4 @@
# See https://sdkman.io/usage#config
# A summary is to add the following to ~/.sdkman/etc/config
# sdkman_auto_env=true
-java=17.0.14-tem
\ No newline at end of file
+java=21-tem
\ No newline at end of file
diff --git a/langchain4j-ollama-spring-boot-starter/pom.xml b/langchain4j-ollama-spring-boot-starter/pom.xml
index 140887e..a929e34 100644
--- a/langchain4j-ollama-spring-boot-starter/pom.xml
+++ b/langchain4j-ollama-spring-boot-starter/pom.xml
@@ -31,6 +31,12 @@
+
+ dev.langchain4j
+ langchain4j-spring-boot-autoconfigure
+ ${project.version}
+
+
dev.langchain4j
langchain4j-http-client-spring-restclient
diff --git a/langchain4j-ollama-spring-boot-starter/src/main/java/dev/langchain4j/ollama/spring/AutoConfig.java b/langchain4j-ollama-spring-boot-starter/src/main/java/dev/langchain4j/ollama/spring/AutoConfig.java
index ee5153f..92f89a1 100644
--- a/langchain4j-ollama-spring-boot-starter/src/main/java/dev/langchain4j/ollama/spring/AutoConfig.java
+++ b/langchain4j-ollama-spring-boot-starter/src/main/java/dev/langchain4j/ollama/spring/AutoConfig.java
@@ -1,48 +1,31 @@
package dev.langchain4j.ollama.spring;
+import dev.langchain4j.autoconfigure.http.HttpClientAutoConfiguration;
import dev.langchain4j.http.client.HttpClientBuilder;
-import dev.langchain4j.http.client.spring.restclient.SpringRestClient;
import dev.langchain4j.model.chat.listener.ChatModelListener;
-import dev.langchain4j.model.ollama.*;
+import dev.langchain4j.model.ollama.OllamaChatModel;
+import dev.langchain4j.model.ollama.OllamaEmbeddingModel;
+import dev.langchain4j.model.ollama.OllamaLanguageModel;
+import dev.langchain4j.model.ollama.OllamaStreamingChatModel;
+import dev.langchain4j.model.ollama.OllamaStreamingLanguageModel;
import org.springframework.beans.factory.ObjectProvider;
-import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.AutoConfiguration;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
-import org.springframework.core.task.AsyncTaskExecutor;
-import org.springframework.core.task.support.ContextPropagatingTaskDecorator;
-import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
-import org.springframework.web.client.RestClient;
import static dev.langchain4j.ollama.spring.Properties.PREFIX;
-@AutoConfiguration(after = RestClientAutoConfiguration.class)
+@AutoConfiguration(after = HttpClientAutoConfiguration.class)
@EnableConfigurationProperties(Properties.class)
public class AutoConfig {
- private static final String TASK_EXECUTOR_THREAD_NAME_PREFIX = "LangChain4j-Ollama-";
-
- private static final String CHAT_MODEL_HTTP_CLIENT_BUILDER = "ollamaChatModelHttpClientBuilder";
-
- private static final String STREAMING_CHAT_MODEL_HTTP_CLIENT_BUILDER = "ollamaStreamingChatModelHttpClientBuilder";
- private static final String STREAMING_CHAT_MODEL_TASK_EXECUTOR = "ollamaStreamingChatModelTaskExecutor";
-
- private static final String LANGUAGE_MODEL_HTTP_CLIENT_BUILDER = "ollamaLanguageModelHttpClientBuilder";
-
- private static final String STREAMING_LANGUAGE_MODEL_HTTP_CLIENT_BUILDER = "ollamaStreamingLanguageModelHttpClientBuilder";
- private static final String STREAMING_LANGUAGE_MODEL_TASK_EXECUTOR = "ollamaStreamingLanguageModelTaskExecutor";
-
- private static final String EMBEDDING_MODEL_HTTP_CLIENT_BUILDER = "ollamaEmbeddingModelHttpClientBuilder";
-
@Bean
+ @ConditionalOnMissingBean
@ConditionalOnProperty(PREFIX + ".chat-model.base-url")
OllamaChatModel ollamaChatModel(
- @Qualifier(CHAT_MODEL_HTTP_CLIENT_BUILDER) HttpClientBuilder httpClientBuilder,
+ HttpClientBuilder httpClientBuilder,
Properties properties,
ObjectProvider listeners
) {
@@ -69,20 +52,11 @@ OllamaChatModel ollamaChatModel(
.build();
}
- @Bean(CHAT_MODEL_HTTP_CLIENT_BUILDER)
- @ConditionalOnProperty(PREFIX + ".chat-model.base-url")
- @ConditionalOnMissingBean(name = CHAT_MODEL_HTTP_CLIENT_BUILDER)
- HttpClientBuilder ollamaChatModelHttpClientBuilder(ObjectProvider restClientBuilder) {
- return SpringRestClient.builder()
- .restClientBuilder(restClientBuilder.getIfAvailable(RestClient::builder))
- // executor is not needed for no-streaming OllamaChatModel
- .createDefaultStreamingRequestExecutor(false);
- }
-
@Bean
+ @ConditionalOnMissingBean
@ConditionalOnProperty(PREFIX + ".streaming-chat-model.base-url")
OllamaStreamingChatModel ollamaStreamingChatModel(
- @Qualifier(STREAMING_CHAT_MODEL_HTTP_CLIENT_BUILDER) HttpClientBuilder httpClientBuilder,
+ HttpClientBuilder httpClientBuilder,
Properties properties,
ObjectProvider listeners
) {
@@ -108,42 +82,11 @@ OllamaStreamingChatModel ollamaStreamingChatModel(
.build();
}
- @Bean(STREAMING_CHAT_MODEL_HTTP_CLIENT_BUILDER)
- @ConditionalOnProperty(PREFIX + ".streaming-chat-model.base-url")
- @ConditionalOnMissingBean(name = STREAMING_CHAT_MODEL_HTTP_CLIENT_BUILDER)
- HttpClientBuilder ollamaStreamingChatModelHttpClientBuilder(
- ObjectProvider restClientBuilder,
- @Qualifier(STREAMING_CHAT_MODEL_TASK_EXECUTOR) AsyncTaskExecutor executor) {
- return SpringRestClient.builder()
- .restClientBuilder(restClientBuilder.getIfAvailable(RestClient::builder))
- .streamingRequestExecutor(executor);
- }
-
- @Bean(STREAMING_CHAT_MODEL_TASK_EXECUTOR)
- @ConditionalOnProperty(PREFIX + ".streaming-chat-model.base-url")
- @ConditionalOnMissingBean(name = STREAMING_CHAT_MODEL_TASK_EXECUTOR)
- @ConditionalOnClass(name = "io.micrometer.context.ContextSnapshotFactory")
- AsyncTaskExecutor ollamaStreamingChatModelTaskExecutorWithContextPropagation() {
- ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
- taskExecutor.setThreadNamePrefix(TASK_EXECUTOR_THREAD_NAME_PREFIX);
- taskExecutor.setTaskDecorator(new ContextPropagatingTaskDecorator());
- return taskExecutor;
- }
-
- @Bean(STREAMING_CHAT_MODEL_TASK_EXECUTOR)
- @ConditionalOnProperty(PREFIX + ".streaming-chat-model.base-url")
- @ConditionalOnMissingBean(name = STREAMING_CHAT_MODEL_TASK_EXECUTOR)
- @ConditionalOnMissingClass("io.micrometer.context.ContextSnapshotFactory")
- AsyncTaskExecutor ollamaStreamingChatModelTaskExecutor() {
- ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
- taskExecutor.setThreadNamePrefix(TASK_EXECUTOR_THREAD_NAME_PREFIX);
- return taskExecutor;
- }
-
@Bean
+ @ConditionalOnMissingBean
@ConditionalOnProperty(PREFIX + ".language-model.base-url")
OllamaLanguageModel ollamaLanguageModel(
- @Qualifier(LANGUAGE_MODEL_HTTP_CLIENT_BUILDER) HttpClientBuilder httpClientBuilder,
+ HttpClientBuilder httpClientBuilder,
Properties properties
) {
LanguageModelProperties languageModelProperties = properties.getLanguageModel();
@@ -167,20 +110,10 @@ OllamaLanguageModel ollamaLanguageModel(
.build();
}
- @Bean(LANGUAGE_MODEL_HTTP_CLIENT_BUILDER)
- @ConditionalOnProperty(PREFIX + ".language-model.base-url")
- @ConditionalOnMissingBean(name = LANGUAGE_MODEL_HTTP_CLIENT_BUILDER)
- HttpClientBuilder ollamaLanguageModelHttpClientBuilder(ObjectProvider restClientBuilder) {
- return SpringRestClient.builder()
- .restClientBuilder(restClientBuilder.getIfAvailable(RestClient::builder))
- // executor is not needed for no-streaming OllamaLanguageModel
- .createDefaultStreamingRequestExecutor(false);
- }
-
@Bean
@ConditionalOnProperty(PREFIX + ".streaming-language-model.base-url")
OllamaStreamingLanguageModel ollamaStreamingLanguageModel(
- @Qualifier(STREAMING_LANGUAGE_MODEL_HTTP_CLIENT_BUILDER) HttpClientBuilder httpClientBuilder,
+ HttpClientBuilder httpClientBuilder,
Properties properties
) {
LanguageModelProperties languageModelProperties = properties.getStreamingLanguageModel();
@@ -203,43 +136,11 @@ OllamaStreamingLanguageModel ollamaStreamingLanguageModel(
.build();
}
- @Bean(STREAMING_LANGUAGE_MODEL_HTTP_CLIENT_BUILDER)
- @ConditionalOnProperty(PREFIX + ".streaming-language-model.base-url")
- @ConditionalOnMissingBean(name = STREAMING_LANGUAGE_MODEL_HTTP_CLIENT_BUILDER)
- HttpClientBuilder ollamaStreamingLanguageModelHttpClientBuilder(
- @Qualifier(STREAMING_LANGUAGE_MODEL_TASK_EXECUTOR) AsyncTaskExecutor executor,
- ObjectProvider restClientBuilder
- ) {
- return SpringRestClient.builder()
- .restClientBuilder(restClientBuilder.getIfAvailable(RestClient::builder))
- .streamingRequestExecutor(executor);
- }
-
- @Bean(STREAMING_LANGUAGE_MODEL_TASK_EXECUTOR)
- @ConditionalOnProperty(PREFIX + ".streaming-language-model.base-url")
- @ConditionalOnMissingBean(name = STREAMING_LANGUAGE_MODEL_TASK_EXECUTOR)
- @ConditionalOnClass(name = "io.micrometer.context.ContextSnapshotFactory")
- AsyncTaskExecutor ollamaStreamingLanguageModelTaskExecutorWithContextPropagation() {
- ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
- taskExecutor.setThreadNamePrefix(TASK_EXECUTOR_THREAD_NAME_PREFIX);
- taskExecutor.setTaskDecorator(new ContextPropagatingTaskDecorator());
- return taskExecutor;
- }
-
- @Bean(STREAMING_LANGUAGE_MODEL_TASK_EXECUTOR)
- @ConditionalOnProperty(PREFIX + ".streaming-language-model.base-url")
- @ConditionalOnMissingBean(name = STREAMING_LANGUAGE_MODEL_TASK_EXECUTOR)
- @ConditionalOnMissingClass("io.micrometer.context.ContextSnapshotFactory")
- AsyncTaskExecutor ollamaStreamingLanguageModelTaskExecutor() {
- ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
- taskExecutor.setThreadNamePrefix(TASK_EXECUTOR_THREAD_NAME_PREFIX);
- return taskExecutor;
- }
-
@Bean
+ @ConditionalOnMissingBean
@ConditionalOnProperty(PREFIX + ".embedding-model.base-url")
OllamaEmbeddingModel ollamaEmbeddingModel(
- @Qualifier(EMBEDDING_MODEL_HTTP_CLIENT_BUILDER) HttpClientBuilder httpClientBuilder,
+ HttpClientBuilder httpClientBuilder,
Properties properties
) {
EmbeddingModelProperties embeddingModelProperties = properties.getEmbeddingModel();
@@ -255,13 +156,4 @@ OllamaEmbeddingModel ollamaEmbeddingModel(
.build();
}
- @Bean(EMBEDDING_MODEL_HTTP_CLIENT_BUILDER)
- @ConditionalOnProperty(PREFIX + ".embedding-model.base-url")
- @ConditionalOnMissingBean(name = EMBEDDING_MODEL_HTTP_CLIENT_BUILDER)
- HttpClientBuilder ollamaEmbeddingModelHttpClientBuilder(ObjectProvider restClientBuilder) {
- return SpringRestClient.builder()
- .restClientBuilder(restClientBuilder.getIfAvailable(RestClient::builder))
- // executor is not needed for no-streaming OllamaEmbeddingModel
- .createDefaultStreamingRequestExecutor(false);
- }
-}
\ No newline at end of file
+}
diff --git a/langchain4j-ollama-spring-boot-starter/src/test/java/dev/langchain4j/ollama/spring/AutoConfigIT.java b/langchain4j-ollama-spring-boot-starter/src/test/java/dev/langchain4j/ollama/spring/AutoConfigIT.java
index 7204454..dd3a114 100644
--- a/langchain4j-ollama-spring-boot-starter/src/test/java/dev/langchain4j/ollama/spring/AutoConfigIT.java
+++ b/langchain4j-ollama-spring-boot-starter/src/test/java/dev/langchain4j/ollama/spring/AutoConfigIT.java
@@ -1,5 +1,6 @@
package dev.langchain4j.ollama.spring;
+import dev.langchain4j.autoconfigure.http.HttpClientAutoConfiguration;
import dev.langchain4j.model.StreamingResponseHandler;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.chat.StreamingChatLanguageModel;
@@ -65,7 +66,7 @@ private static String baseUrl() {
}
ApplicationContextRunner contextRunner = new ApplicationContextRunner()
- .withConfiguration(AutoConfigurations.of(AutoConfig.class));
+ .withConfiguration(AutoConfigurations.of(HttpClientAutoConfiguration.class, AutoConfig.class));
@Test
void should_provide_chat_model() {
@@ -189,7 +190,7 @@ void should_provide_streaming_chat_model_with_custom_task_executor() {
ThreadPoolTaskExecutor customExecutor = spy(new ThreadPoolTaskExecutor());
contextRunner
- .withBean("ollamaStreamingChatModelTaskExecutor", ThreadPoolTaskExecutor.class, () -> customExecutor)
+ .withBean("httpClientTaskExecutor", ThreadPoolTaskExecutor.class, () -> customExecutor)
.withPropertyValues(
"langchain4j.ollama.streaming-chat-model.base-url=" + baseUrl(),
"langchain4j.ollama.streaming-chat-model.model-name=" + MODEL_NAME,
diff --git a/langchain4j-spring-boot-autoconfigure/pom.xml b/langchain4j-spring-boot-autoconfigure/pom.xml
new file mode 100644
index 0000000..5011e47
--- /dev/null
+++ b/langchain4j-spring-boot-autoconfigure/pom.xml
@@ -0,0 +1,60 @@
+
+
+ 4.0.0
+
+
+ dev.langchain4j
+ langchain4j-spring
+ 1.0.0-beta2-SNAPSHOT
+ ../pom.xml
+
+
+ langchain4j-spring-boot-autoconfigure
+ LangChain4j :: Spring Boot AutoConfiguration
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ dev.langchain4j
+ langchain4j-http-client
+ true
+
+
+
+ dev.langchain4j
+ langchain4j-http-client-spring-restclient
+ ${project.version}
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-autoconfigure-processor
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
diff --git a/langchain4j-spring-boot-autoconfigure/src/main/java/dev/langchain4j/autoconfigure/http/HttpClientAutoConfiguration.java b/langchain4j-spring-boot-autoconfigure/src/main/java/dev/langchain4j/autoconfigure/http/HttpClientAutoConfiguration.java
new file mode 100644
index 0000000..bd9da23
--- /dev/null
+++ b/langchain4j-spring-boot-autoconfigure/src/main/java/dev/langchain4j/autoconfigure/http/HttpClientAutoConfiguration.java
@@ -0,0 +1,94 @@
+package dev.langchain4j.autoconfigure.http;
+
+import dev.langchain4j.http.client.HttpClientBuilder;
+import dev.langchain4j.http.client.spring.restclient.SpringRestClientBuilder;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.beans.factory.config.ConfigurableBeanFactory;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading;
+import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
+import org.springframework.boot.autoconfigure.thread.Threading;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.task.SimpleAsyncTaskExecutorBuilder;
+import org.springframework.boot.task.ThreadPoolTaskExecutorBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Scope;
+import org.springframework.core.task.AsyncTaskExecutor;
+import org.springframework.core.task.TaskDecorator;
+import org.springframework.core.task.support.ContextPropagatingTaskDecorator;
+import org.springframework.web.client.RestClient;
+
+/**
+ * Auto-configuration for {@link HttpClientBuilder}.
+ */
+@AutoConfiguration(after = { RestClientAutoConfiguration.class, TaskExecutionAutoConfiguration.class })
+public class HttpClientAutoConfiguration {
+
+ static final String TASK_EXECUTOR_BEAN_NAME = "httpClientTaskExecutor";
+ static final String TASK_EXECUTOR_THREAD_NAME_PREFIX = "langchain4j-http-";
+
+ /**
+ * A {@link HttpClientBuilder} bean that is used to create {@link dev.langchain4j.http.client.HttpClient}s.
+ * It's a prototype bean (not a singleton) to allow for customizing the builder
+ * per {@link dev.langchain4j.http.client.HttpClient} instance.
+ */
+ @Bean
+ @ConditionalOnMissingBean
+ @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
+ HttpClientBuilder httpClientBuilder(
+ @Qualifier(TASK_EXECUTOR_BEAN_NAME) AsyncTaskExecutor asyncTaskExecutor,
+ ObjectProvider restClientBuilder,
+ ObjectProvider customizers
+ ) {
+ HttpClientBuilder httpClientBuilder = new SpringRestClientBuilder()
+ .streamingRequestExecutor(asyncTaskExecutor)
+ .restClientBuilder(restClientBuilder.getIfAvailable(RestClient::builder));
+ customizers.orderedStream().forEach(customizer -> customizer.customize(httpClientBuilder));
+ return httpClientBuilder;
+ }
+
+ @Bean(TASK_EXECUTOR_BEAN_NAME)
+ @ConditionalOnMissingBean(name = TASK_EXECUTOR_BEAN_NAME)
+ @ConditionalOnThreading(Threading.VIRTUAL)
+ AsyncTaskExecutor httpClientVirtualThreadsTaskExecutor(ObjectProvider builder, ObjectProvider taskDecorator) {
+ return builder.getIfAvailable(() -> defaultSimpleAsyncTaskExecutorBuilder(taskDecorator))
+ .threadNamePrefix(TASK_EXECUTOR_THREAD_NAME_PREFIX)
+ .build();
+ }
+
+ @Bean(TASK_EXECUTOR_BEAN_NAME)
+ @ConditionalOnMissingBean(name = TASK_EXECUTOR_BEAN_NAME)
+ @ConditionalOnThreading(Threading.PLATFORM)
+ AsyncTaskExecutor httpClientThreadPoolTaskExecutor(ObjectProvider builder, ObjectProvider taskDecorator) {
+ return builder.getIfAvailable(() -> defaultThreadPoolTaskExecutorBuilder(taskDecorator))
+ .threadNamePrefix(TASK_EXECUTOR_THREAD_NAME_PREFIX)
+ .build();
+ }
+
+ /**
+ * This is picked up by {@link TaskExecutionAutoConfiguration}.
+ * In case the autoconfiguration is disabled, we use this bean
+ * explicitly to build default {@link AsyncTaskExecutor}s.
+ */
+ @Bean
+ @ConditionalOnClass(name = "io.micrometer.context.ContextSnapshotFactory")
+ TaskDecorator contextPropagatingTaskDecorator() {
+ return new ContextPropagatingTaskDecorator();
+ }
+
+ private SimpleAsyncTaskExecutorBuilder defaultSimpleAsyncTaskExecutorBuilder(ObjectProvider taskDecorator) {
+ var builder = new SimpleAsyncTaskExecutorBuilder().virtualThreads(true);
+ taskDecorator.ifAvailable(builder::taskDecorator);
+ return builder;
+ }
+
+ private ThreadPoolTaskExecutorBuilder defaultThreadPoolTaskExecutorBuilder(ObjectProvider taskDecorator) {
+ var builder = new ThreadPoolTaskExecutorBuilder();
+ taskDecorator.ifAvailable(builder::taskDecorator);
+ return builder;
+ }
+
+}
diff --git a/langchain4j-spring-boot-autoconfigure/src/main/java/dev/langchain4j/autoconfigure/http/HttpClientBuilderCustomizer.java b/langchain4j-spring-boot-autoconfigure/src/main/java/dev/langchain4j/autoconfigure/http/HttpClientBuilderCustomizer.java
new file mode 100644
index 0000000..c539f22
--- /dev/null
+++ b/langchain4j-spring-boot-autoconfigure/src/main/java/dev/langchain4j/autoconfigure/http/HttpClientBuilderCustomizer.java
@@ -0,0 +1,15 @@
+package dev.langchain4j.autoconfigure.http;
+
+import dev.langchain4j.http.client.HttpClient;
+import dev.langchain4j.http.client.HttpClientBuilder;
+
+/**
+ * Callback to customize the {@link HttpClientBuilder}
+ * used to create the auto-configured {@link HttpClient}.
+ */
+@FunctionalInterface
+public interface HttpClientBuilderCustomizer {
+
+ void customize(HttpClientBuilder builder);
+
+}
diff --git a/langchain4j-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/langchain4j-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..0c1e754
--- /dev/null
+++ b/langchain4j-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+dev.langchain4j.autoconfigure.http.HttpClientAutoConfiguration
\ No newline at end of file
diff --git a/langchain4j-spring-boot-autoconfigure/src/test/java/dev/langchain4j/autoconfigure/http/HttpClientAutoConfigurationTests.java b/langchain4j-spring-boot-autoconfigure/src/test/java/dev/langchain4j/autoconfigure/http/HttpClientAutoConfigurationTests.java
new file mode 100644
index 0000000..0965341
--- /dev/null
+++ b/langchain4j-spring-boot-autoconfigure/src/test/java/dev/langchain4j/autoconfigure/http/HttpClientAutoConfigurationTests.java
@@ -0,0 +1,89 @@
+package dev.langchain4j.autoconfigure.http;
+
+import dev.langchain4j.http.client.HttpClientBuilder;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledForJreRange;
+import org.junit.jupiter.api.condition.JRE;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.core.task.AsyncTaskExecutor;
+import org.springframework.core.task.SimpleAsyncTaskExecutor;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import static dev.langchain4j.autoconfigure.http.HttpClientAutoConfiguration.TASK_EXECUTOR_BEAN_NAME;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Unit tests for {@link HttpClientAutoConfiguration}.
+ */
+class HttpClientAutoConfigurationTests {
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(HttpClientAutoConfiguration.class));
+
+ @Test
+ void httpClientBuilderWhenAutoConfiguredRestClient() {
+ contextRunner
+ .withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context).hasSingleBean(HttpClientBuilder.class);
+ });
+ }
+
+ @Test
+ void httpClientBuilderWhenNoAutoConfiguredRestClient() {
+ contextRunner
+ .run(context -> {
+ assertThat(context).hasSingleBean(HttpClientBuilder.class);
+ });
+ }
+
+ @Test
+ void httpClientBuilderWhenAutoConfiguredThreadPoolTaskExecutor() {
+ contextRunner
+ .withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context).hasSingleBean(HttpClientBuilder.class);
+ assertThat(context).getBeans(AsyncTaskExecutor.class).hasSize(2);
+ assertThat(context).getBean(TASK_EXECUTOR_BEAN_NAME).isInstanceOf(ThreadPoolTaskExecutor.class);
+ });
+ }
+
+ @Test
+ void httpClientBuilderWhenNoAutoConfiguredThreadPoolTaskExecutor() {
+ contextRunner
+ .run(context -> {
+ assertThat(context).hasSingleBean(HttpClientBuilder.class);
+ assertThat(context).hasSingleBean(AsyncTaskExecutor.class);
+ assertThat(context).getBean(TASK_EXECUTOR_BEAN_NAME).isInstanceOf(ThreadPoolTaskExecutor.class);
+ });
+ }
+
+ @Test
+ @EnabledForJreRange(min = JRE.JAVA_21)
+ void httpClientBuilderWhenAutoConfiguredVirtualThreadsTaskExecutor() {
+ contextRunner
+ .withPropertyValues("spring.threads.virtual.enabled=true")
+ .withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context).hasSingleBean(HttpClientBuilder.class);
+ assertThat(context).getBeans(AsyncTaskExecutor.class).hasSize(2);
+ assertThat(context).getBean(TASK_EXECUTOR_BEAN_NAME).isInstanceOf(SimpleAsyncTaskExecutor.class);
+ });
+ }
+
+ @Test
+ @EnabledForJreRange(min = JRE.JAVA_21)
+ void httpClientBuilderWhenNoAutoConfiguredVirtualThreadsTaskExecutor() {
+ contextRunner
+ .withPropertyValues("spring.threads.virtual.enabled=true")
+ .run(context -> {
+ assertThat(context).hasSingleBean(HttpClientBuilder.class);
+ assertThat(context).hasSingleBean(AsyncTaskExecutor.class);
+ assertThat(context).getBean(TASK_EXECUTOR_BEAN_NAME).isInstanceOf(SimpleAsyncTaskExecutor.class);
+ });
+ }
+
+}
diff --git a/pom.xml b/pom.xml
index 1835848..93d05d7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -14,6 +14,7 @@
https://github.com/langchain4j/langchain4j-spring
+ langchain4j-spring-boot-autoconfigure
langchain4j-spring-boot-starter
langchain4j-spring-boot-tests
langchain4j-http-client-spring-restclient