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