diff --git a/langchain4j-easy-rag-spring-boot-starter/pom.xml b/langchain4j-easy-rag-spring-boot-starter/pom.xml
index 0b71a52c..78879508 100644
--- a/langchain4j-easy-rag-spring-boot-starter/pom.xml
+++ b/langchain4j-easy-rag-spring-boot-starter/pom.xml
@@ -53,7 +53,7 @@
org.testcontainers
- junit-jupiter
+ testcontainers-junit-jupiter
test
diff --git a/langchain4j-elasticsearch-spring-boot-starter/pom.xml b/langchain4j-elasticsearch-spring-boot-starter/pom.xml
index 0bff17f0..d3197b92 100644
--- a/langchain4j-elasticsearch-spring-boot-starter/pom.xml
+++ b/langchain4j-elasticsearch-spring-boot-starter/pom.xml
@@ -15,7 +15,6 @@
jar
-
8.19.2
+ ${elastic.version}
+
+
org.springframework.boot
spring-boot-starter
@@ -82,7 +88,7 @@
org.testcontainers
- elasticsearch
+ testcontainers-elasticsearch
test
diff --git a/langchain4j-http-client-spring-restclient/pom.xml b/langchain4j-http-client-spring-restclient/pom.xml
index 7f139c34..f1d99489 100644
--- a/langchain4j-http-client-spring-restclient/pom.xml
+++ b/langchain4j-http-client-spring-restclient/pom.xml
@@ -28,7 +28,7 @@
org.springframework.boot
- spring-boot
+ spring-boot-starter-restclient
@@ -58,17 +58,22 @@
test
+
+ org.springframework.boot
+ spring-boot-starter-restclient-test
+ test
+
+
io.projectreactor.netty
reactor-netty
test
-
- org.wiremock
- wiremock-jetty12
- 3.10.0
+ org.wiremock.integrations
+ wiremock-spring-boot
+ 4.1.0
test
diff --git a/langchain4j-http-client-spring-restclient/src/main/java/dev/langchain4j/http/client/spring/restclient/HttpClientSettingsStrategy.java b/langchain4j-http-client-spring-restclient/src/main/java/dev/langchain4j/http/client/spring/restclient/HttpClientSettingsStrategy.java
new file mode 100644
index 00000000..543418c6
--- /dev/null
+++ b/langchain4j-http-client-spring-restclient/src/main/java/dev/langchain4j/http/client/spring/restclient/HttpClientSettingsStrategy.java
@@ -0,0 +1,17 @@
+package dev.langchain4j.http.client.spring.restclient;
+
+import org.springframework.http.client.ClientHttpRequestFactory;
+
+import java.time.Duration;
+
+/**
+ * Strategy interface for creating a {@link ClientHttpRequestFactory} with the appropriate
+ * Spring Boot API, depending on the version available on the classpath.
+ *
+ * @see SpringBoot4HttpClientSettings
+ * @see SpringBoot3HttpClientSettings
+ */
+interface HttpClientSettingsStrategy {
+
+ ClientHttpRequestFactory createRequestFactory(Duration connectTimeout, Duration readTimeout);
+}
diff --git a/langchain4j-http-client-spring-restclient/src/main/java/dev/langchain4j/http/client/spring/restclient/SpringBoot3HttpClientSettings.java b/langchain4j-http-client-spring-restclient/src/main/java/dev/langchain4j/http/client/spring/restclient/SpringBoot3HttpClientSettings.java
new file mode 100644
index 00000000..2498a153
--- /dev/null
+++ b/langchain4j-http-client-spring-restclient/src/main/java/dev/langchain4j/http/client/spring/restclient/SpringBoot3HttpClientSettings.java
@@ -0,0 +1,74 @@
+package dev.langchain4j.http.client.spring.restclient;
+
+import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
+import org.springframework.http.client.ClientHttpRequestFactory;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.time.Duration;
+
+/**
+ * The Spring Boot 3.5+ implementation uses reflection because Spring Boot 4 is no longer included in the langchain4J-spring classpath.
+ */
+class SpringBoot3HttpClientSettings implements HttpClientSettingsStrategy {
+
+ private static final Class> DEPRECATED_SETTINGS_CLASS;
+ private static final Class> NEW_SETTINGS_CLASS;
+ private static final Field DEFAULTS_FIELD;
+ private static final Method WITH_CONNECT_TIMEOUT_METHOD;
+ private static final Method WITH_READ_TIMEOUT_METHOD;
+ private static final Method ADAPT_METHOD;
+ private static final Method BUILD_METHOD;
+
+ static {
+ try {
+ DEPRECATED_SETTINGS_CLASS = Class.forName("org.springframework.boot.web.client.ClientHttpRequestFactorySettings");
+ NEW_SETTINGS_CLASS = Class.forName("org.springframework.boot.http.client.ClientHttpRequestFactorySettings");
+
+ DEFAULTS_FIELD = DEPRECATED_SETTINGS_CLASS.getField("DEFAULTS");
+ WITH_CONNECT_TIMEOUT_METHOD = DEPRECATED_SETTINGS_CLASS.getMethod("withConnectTimeout", Duration.class);
+ WITH_READ_TIMEOUT_METHOD = DEPRECATED_SETTINGS_CLASS.getMethod("withReadTimeout", Duration.class);
+ ADAPT_METHOD = DEPRECATED_SETTINGS_CLASS.getDeclaredMethod("adapt");
+ ADAPT_METHOD.setAccessible(true);
+
+ ClientHttpRequestFactoryBuilder> builder = ClientHttpRequestFactoryBuilder.detect();
+ BUILD_METHOD = findBuildMethod(builder.getClass(), NEW_SETTINGS_CLASS);
+ BUILD_METHOD.setAccessible(true);
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalStateException("Failed to initialize SpringBoot3HttpClientSettings", e);
+ }
+ }
+
+ @Override
+ public ClientHttpRequestFactory createRequestFactory(Duration connectTimeout, Duration readTimeout) {
+ try {
+ Object deprecatedSettings = DEFAULTS_FIELD.get(null);
+
+ if (connectTimeout != null) {
+ deprecatedSettings = WITH_CONNECT_TIMEOUT_METHOD.invoke(deprecatedSettings, connectTimeout);
+ }
+ if (readTimeout != null) {
+ deprecatedSettings = WITH_READ_TIMEOUT_METHOD.invoke(deprecatedSettings, readTimeout);
+ }
+
+ Object newSettings = ADAPT_METHOD.invoke(deprecatedSettings);
+ return (ClientHttpRequestFactory) BUILD_METHOD.invoke(ClientHttpRequestFactoryBuilder.detect(), newSettings);
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalStateException("Failed to create ClientHttpRequestFactory for Spring Boot 3.x", e);
+ }
+ }
+
+ private static Method findBuildMethod(Class> clazz, Class> newSettingsClass) throws NoSuchMethodException {
+ for (Method method : clazz.getMethods()) {
+ if ("build".equals(method.getName())
+ && method.getParameterCount() == 1
+ && method.getParameterTypes()[0].isAssignableFrom(newSettingsClass)) {
+ return method;
+ }
+ }
+ if (clazz.getSuperclass() != null) {
+ return findBuildMethod(clazz.getSuperclass(), newSettingsClass);
+ }
+ throw new NoSuchMethodException("No build method accepting " + newSettingsClass.getName());
+ }
+}
diff --git a/langchain4j-http-client-spring-restclient/src/main/java/dev/langchain4j/http/client/spring/restclient/SpringBoot4HttpClientSettings.java b/langchain4j-http-client-spring-restclient/src/main/java/dev/langchain4j/http/client/spring/restclient/SpringBoot4HttpClientSettings.java
new file mode 100644
index 00000000..f6dcc4f4
--- /dev/null
+++ b/langchain4j-http-client-spring-restclient/src/main/java/dev/langchain4j/http/client/spring/restclient/SpringBoot4HttpClientSettings.java
@@ -0,0 +1,23 @@
+package dev.langchain4j.http.client.spring.restclient;
+
+import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
+import org.springframework.boot.http.client.HttpClientSettings;
+import org.springframework.http.client.ClientHttpRequestFactory;
+
+import java.time.Duration;
+
+class SpringBoot4HttpClientSettings implements HttpClientSettingsStrategy {
+
+ @Override
+ public ClientHttpRequestFactory createRequestFactory(Duration connectTimeout, Duration readTimeout) {
+ HttpClientSettings settings = HttpClientSettings.defaults();
+ if (connectTimeout != null) {
+ settings = settings.withConnectTimeout(connectTimeout);
+ }
+ if (readTimeout != null) {
+ settings = settings.withReadTimeout(readTimeout);
+ }
+ return ClientHttpRequestFactoryBuilder.detect()
+ .build(settings);
+ }
+}
diff --git a/langchain4j-http-client-spring-restclient/src/main/java/dev/langchain4j/http/client/spring/restclient/SpringBootHttpClientSettingsHelper.java b/langchain4j-http-client-spring-restclient/src/main/java/dev/langchain4j/http/client/spring/restclient/SpringBootHttpClientSettingsHelper.java
new file mode 100644
index 00000000..5db1a89d
--- /dev/null
+++ b/langchain4j-http-client-spring-restclient/src/main/java/dev/langchain4j/http/client/spring/restclient/SpringBootHttpClientSettingsHelper.java
@@ -0,0 +1,40 @@
+package dev.langchain4j.http.client.spring.restclient;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.SpringBootVersion;
+import org.springframework.http.client.ClientHttpRequestFactory;
+
+import java.time.Duration;
+
+/**
+ * Factory that detects the Spring Boot version on the classpath and delegates
+ * {@link ClientHttpRequestFactory} creation to the appropriate strategy.
+ */
+public final class SpringBootHttpClientSettingsHelper {
+
+ private static final Logger log = LoggerFactory.getLogger(SpringBootHttpClientSettingsHelper.class);
+
+ private static final HttpClientSettingsStrategy STRATEGY;
+
+ static {
+ String version = SpringBootVersion.getVersion();
+ int majorVersion = Integer.parseInt(version.split("\\.")[0]);
+ log.debug("Detected Spring Boot major version {}", majorVersion);
+
+ HttpClientSettingsStrategy detected;
+ if (majorVersion >= 4) {
+ detected = new SpringBoot4HttpClientSettings();
+ } else {
+ detected = new SpringBoot3HttpClientSettings();
+ }
+ STRATEGY = detected;
+ }
+
+ private SpringBootHttpClientSettingsHelper() {
+ }
+
+ public static ClientHttpRequestFactory createClientHttpRequestFactory(Duration connectTimeout, Duration readTimeout) {
+ return STRATEGY.createRequestFactory(connectTimeout, readTimeout);
+ }
+}
diff --git a/langchain4j-http-client-spring-restclient/src/main/java/dev/langchain4j/http/client/spring/restclient/SpringRestClient.java b/langchain4j-http-client-spring-restclient/src/main/java/dev/langchain4j/http/client/spring/restclient/SpringRestClient.java
index df79dba6..44629c43 100644
--- a/langchain4j-http-client-spring-restclient/src/main/java/dev/langchain4j/http/client/spring/restclient/SpringRestClient.java
+++ b/langchain4j-http-client-spring-restclient/src/main/java/dev/langchain4j/http/client/spring/restclient/SpringRestClient.java
@@ -8,10 +8,9 @@
import dev.langchain4j.http.client.SuccessfulHttpResponse;
import dev.langchain4j.http.client.sse.ServerSentEventListener;
import dev.langchain4j.http.client.sse.ServerSentEventParser;
-import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
-import org.springframework.boot.http.client.ClientHttpRequestFactorySettings;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.task.AsyncTaskExecutor;
+import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@@ -22,8 +21,11 @@
import java.io.InputStream;
import java.net.SocketTimeoutException;
+import java.util.List;
import java.util.Map;
+import java.util.stream.Collectors;
+import static dev.langchain4j.http.client.spring.restclient.SpringBootHttpClientSettingsHelper.createClientHttpRequestFactory;
import static dev.langchain4j.http.client.sse.ServerSentEventListenerUtils.ignoringExceptions;
import static dev.langchain4j.internal.Utils.getOrDefault;
@@ -36,21 +38,17 @@ public SpringRestClient(SpringRestClientBuilder builder) {
RestClient.Builder restClientBuilder = getOrDefault(builder.restClientBuilder(), RestClient::builder);
- ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.defaults();
- if (builder.connectTimeout() != null) {
- settings = settings.withConnectTimeout(builder.connectTimeout());
- }
- if (builder.readTimeout() != null) {
- settings = settings.withReadTimeout(builder.readTimeout());
- }
- ClientHttpRequestFactory clientHttpRequestFactory = ClientHttpRequestFactoryBuilder.detect().build(settings);
+ ClientHttpRequestFactory clientHttpRequestFactory = createClientHttpRequestFactory(
+ builder.connectTimeout(),
+ builder.readTimeout()
+ );
this.delegate = restClientBuilder
.requestFactory(clientHttpRequestFactory)
.build();
this.streamingRequestExecutor = getOrDefault(builder.streamingRequestExecutor(), () -> {
- if (builder.createDefaultStreamingRequestExecutor()) {
+ if (Boolean.TRUE.equals(builder.createDefaultStreamingRequestExecutor())) {
return createDefaultStreamingRequestExecutor();
} else {
return null;
@@ -78,7 +76,7 @@ public SuccessfulHttpResponse execute(HttpRequest request) throws HttpException
return SuccessfulHttpResponse.builder()
.statusCode(responseEntity.getStatusCode().value())
- .headers(responseEntity.getHeaders())
+ .headers(toHeadersMap(responseEntity.getHeaders()))
.body(responseEntity.getBody())
.build();
} catch (RestClientResponseException e) {
@@ -111,7 +109,7 @@ public void execute(HttpRequest request, ServerSentEventParser parser, ServerSen
SuccessfulHttpResponse response = SuccessfulHttpResponse.builder()
.statusCode(statusCode)
- .headers(springResponse.getHeaders())
+ .headers(toHeadersMap(springResponse.getHeaders()))
.build();
ignoringExceptions(() -> listener.onOpen(response));
@@ -161,4 +159,12 @@ private static MultiValueMap toMultiValueMap(Map
}
return multipart;
}
+
+ private static Map> toHeadersMap(HttpHeaders httpHeaders) {
+ return httpHeaders.headerSet().stream()
+ .collect(Collectors.toMap(
+ Map.Entry::getKey,
+ Map.Entry::getValue
+ ));
+ }
}
diff --git a/langchain4j-milvus-spring-boot-starter/pom.xml b/langchain4j-milvus-spring-boot-starter/pom.xml
index 3fe31cf1..ddf6e4a3 100644
--- a/langchain4j-milvus-spring-boot-starter/pom.xml
+++ b/langchain4j-milvus-spring-boot-starter/pom.xml
@@ -62,7 +62,7 @@
org.testcontainers
- milvus
+ testcontainers-milvus
test
diff --git a/langchain4j-ollama-spring-boot-starter/pom.xml b/langchain4j-ollama-spring-boot-starter/pom.xml
index e07c14ec..753eab7d 100644
--- a/langchain4j-ollama-spring-boot-starter/pom.xml
+++ b/langchain4j-ollama-spring-boot-starter/pom.xml
@@ -71,7 +71,7 @@
org.testcontainers
- junit-jupiter
+ testcontainers-junit-jupiter
test
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 f8325a2b..054c120d 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
@@ -11,7 +11,6 @@
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;
@@ -21,7 +20,11 @@
import static dev.langchain4j.ollama.spring.Properties.PREFIX;
-@AutoConfiguration(after = RestClientAutoConfiguration.class)
+@AutoConfiguration(afterName = {
+ "org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration", // Spring Boot 4 support
+ "org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration" // Spring Boot 3 support
+ }
+)
@EnableConfigurationProperties(Properties.class)
public class AutoConfig {
diff --git a/langchain4j-open-ai-spring-boot-starter/src/main/java/dev/langchain4j/openai/spring/AutoConfig.java b/langchain4j-open-ai-spring-boot-starter/src/main/java/dev/langchain4j/openai/spring/AutoConfig.java
index 292f17ec..4dedeb7e 100644
--- a/langchain4j-open-ai-spring-boot-starter/src/main/java/dev/langchain4j/openai/spring/AutoConfig.java
+++ b/langchain4j-open-ai-spring-boot-starter/src/main/java/dev/langchain4j/openai/spring/AutoConfig.java
@@ -11,7 +11,6 @@
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;
@@ -21,7 +20,11 @@
import static dev.langchain4j.openai.spring.Properties.PREFIX;
-@AutoConfiguration(after = RestClientAutoConfiguration.class)
+@AutoConfiguration(afterName = {
+ "org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration", // Spring Boot 4 support
+ "org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration" // Spring Boot 3 support
+ }
+)
@EnableConfigurationProperties(Properties.class)
public class AutoConfig {
diff --git a/langchain4j-spring-boot-starter/pom.xml b/langchain4j-spring-boot-starter/pom.xml
index 422af47e..c99ddc78 100644
--- a/langchain4j-spring-boot-starter/pom.xml
+++ b/langchain4j-spring-boot-starter/pom.xml
@@ -54,7 +54,7 @@
org.springframework.boot
- spring-boot-starter-aop
+ spring-boot-starter-aspectj
test
diff --git a/langchain4j-voyage-ai-spring-boot-starter/pom.xml b/langchain4j-voyage-ai-spring-boot-starter/pom.xml
index d0fa17da..ba2bc942 100644
--- a/langchain4j-voyage-ai-spring-boot-starter/pom.xml
+++ b/langchain4j-voyage-ai-spring-boot-starter/pom.xml
@@ -51,7 +51,7 @@
org.testcontainers
- junit-jupiter
+ testcontainers-junit-jupiter
test
diff --git a/pom.xml b/pom.xml
index 3fff4dc4..3b02792a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -42,7 +42,7 @@
17
17
UTF-8
- 3.5.9
+ 4.0.2
2.7.0