diff --git a/code-execution-engines/langchain4j-code-execution-engine-judge0/pom.xml b/code-execution-engines/langchain4j-code-execution-engine-judge0/pom.xml index 7bb825fb83..930421a92d 100644 --- a/code-execution-engines/langchain4j-code-execution-engine-judge0/pom.xml +++ b/code-execution-engines/langchain4j-code-execution-engine-judge0/pom.xml @@ -22,8 +22,16 @@ - com.squareup.okhttp3 - okhttp + dev.langchain4j + langchain4j-http-client + 1.18.0-SNAPSHOT + + + + dev.langchain4j + langchain4j-http-client-jdk + 1.18.0-SNAPSHOT + runtime diff --git a/code-execution-engines/langchain4j-code-execution-engine-judge0/src/main/java/dev/langchain4j/code/judge0/Judge0JavaScriptEngine.java b/code-execution-engines/langchain4j-code-execution-engine-judge0/src/main/java/dev/langchain4j/code/judge0/Judge0JavaScriptEngine.java index ffa2989e87..9b32a090f7 100644 --- a/code-execution-engines/langchain4j-code-execution-engine-judge0/src/main/java/dev/langchain4j/code/judge0/Judge0JavaScriptEngine.java +++ b/code-execution-engines/langchain4j-code-execution-engine-judge0/src/main/java/dev/langchain4j/code/judge0/Judge0JavaScriptEngine.java @@ -1,15 +1,19 @@ package dev.langchain4j.code.judge0; +import static dev.langchain4j.http.client.HttpMethod.POST; import static dev.langchain4j.internal.Utils.isNullOrBlank; import dev.langchain4j.code.CodeExecutionEngine; -import java.io.IOException; +import dev.langchain4j.exception.HttpException; +import dev.langchain4j.http.client.HttpClient; +import dev.langchain4j.http.client.HttpRequest; +import dev.langchain4j.http.client.HttpClientBuilderLoader; +import dev.langchain4j.http.client.SuccessfulHttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Base64; import java.util.Objects; import java.util.concurrent.TimeUnit; -import okhttp3.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -23,7 +27,6 @@ class Judge0JavaScriptEngine implements CodeExecutionEngine { private static final Logger log = LoggerFactory.getLogger(Judge0JavaScriptEngine.class); // HTTP Constants - private static final MediaType MEDIA_TYPE_JSON = MediaType.parse("application/json"); private static final String RAPID_API_HOST = "judge0-ce.p.rapidapi.com"; private static final String API_URL = "https://judge0-ce.p.rapidapi.com/submissions?base64_encoded=true&wait=true&fields=*"; @@ -46,7 +49,7 @@ class Judge0JavaScriptEngine implements CodeExecutionEngine { private final String apiKey; private final int languageId; - private final OkHttpClient client; + private final HttpClient httpClient; /** * Creates a new Judge0JavaScriptEngine with the specified API key, language ID, and timeout. @@ -65,11 +68,9 @@ class Judge0JavaScriptEngine implements CodeExecutionEngine { this.apiKey = apiKey; this.languageId = languageId; - this.client = new OkHttpClient.Builder() + this.httpClient = HttpClientBuilderLoader.loadHttpClientBuilder() .connectTimeout(timeout) .readTimeout(timeout) - .writeTimeout(timeout) - .callTimeout(timeout) .build(); } @@ -87,8 +88,7 @@ class Judge0JavaScriptEngine implements CodeExecutionEngine { String base64EncodedCode = Base64.getEncoder().encodeToString(code.getBytes(StandardCharsets.UTF_8)); Submission submission = new Submission(languageId, base64EncodedCode); - RequestBody requestBody = RequestBody.create(Json.toJson(submission), MEDIA_TYPE_JSON); - Request request = buildRequest(requestBody); + HttpRequest request = buildRequest(Json.toJson(submission)); return executeWithRetry(request); } @@ -96,15 +96,17 @@ class Judge0JavaScriptEngine implements CodeExecutionEngine { /** * Builds a request to the Judge0 API. * - * @param requestBody The request body + * @param body The request body * @return The built request */ - private Request buildRequest(RequestBody requestBody) { - return new Request.Builder() + private HttpRequest buildRequest(String body) { + return HttpRequest.builder() + .method(POST) .url(API_URL) .addHeader("x-rapidapi-host", RAPID_API_HOST) .addHeader("x-rapidapi-key", apiKey) - .post(requestBody) + .addHeader("Content-Type", "application/json") + .body(body) .build(); } @@ -114,8 +116,8 @@ class Judge0JavaScriptEngine implements CodeExecutionEngine { * @param request The request to execute * @return The result of the request or an error message */ - private String executeWithRetry(Request request) { - Exception lastException = null; + private String executeWithRetry(HttpRequest request) { + RuntimeException lastException = null; for (int attempt = 0; attempt < MAX_RETRIES; attempt++) { if (attempt > 0) { @@ -128,56 +130,44 @@ class Judge0JavaScriptEngine implements CodeExecutionEngine { } } + SuccessfulHttpResponse response; try { - return processRequest(request); - } catch (IOException e) { + response = httpClient.execute(request); + } catch (HttpException e) { + return handleErrorResponse(e); + } catch (RuntimeException e) { lastException = e; log.warn("Request failed (attempt {}/{}): {}", attempt + 1, MAX_RETRIES, e.getMessage()); + continue; } + + String responseBody = response.body(); + if (isNullOrBlank(responseBody)) { + log.warn(ERROR_NULL_RESPONSE); + return ERROR_NULL_RESPONSE; + } + return processResponseBody(responseBody); } log.error("All retry attempts failed", lastException); return "Failed after " + MAX_RETRIES + " attempts: " + lastException.getMessage(); } - /** - * Processes a single request to the Judge0 API. - * - * @param request The request to process - * @return The result of the request or an error message - * @throws IOException if an I/O error occurs - */ - private String processRequest(Request request) throws IOException { - try (Response response = client.newCall(request).execute()) { - if (!response.isSuccessful()) { - return handleErrorResponse(response); - } - - if (response.body() == null) { - log.warn(ERROR_NULL_RESPONSE); - return ERROR_NULL_RESPONSE; - } - - String responseBody = response.body().string(); - return processResponseBody(responseBody); - } - } - /** * Handles error responses from the Judge0 API. * - * @param response The error response + * @param exception The HTTP error carrying the status code and response body * @return An appropriate error message */ - private String handleErrorResponse(Response response) { + private String handleErrorResponse(HttpException exception) { String errorMessage = - switch (response.code()) { + switch (exception.statusCode()) { case 429 -> ERROR_RATE_LIMIT; case 403 -> ERROR_FORBIDDEN; case 404 -> ERROR_NOT_FOUND; case 500 -> ERROR_SERVER; case 503 -> ERROR_UNAVAILABLE; - default -> "Unexpected error code " + response.code() + ": " + response.message(); + default -> "Unexpected error code " + exception.statusCode() + ": " + exception.getMessage(); }; log.warn(errorMessage); diff --git a/langchain4j-jina/pom.xml b/langchain4j-jina/pom.xml index 7b855d8b35..4fd1b0dcda 100644 --- a/langchain4j-jina/pom.xml +++ b/langchain4j-jina/pom.xml @@ -20,13 +20,21 @@ - com.squareup.retrofit2 - retrofit + dev.langchain4j + langchain4j-http-client + 1.18.0-SNAPSHOT - com.squareup.retrofit2 - converter-jackson + dev.langchain4j + langchain4j-http-client-jdk + 1.18.0-SNAPSHOT + runtime + + + + com.fasterxml.jackson.core + jackson-databind diff --git a/langchain4j-jina/revapi.json b/langchain4j-jina/revapi.json index cd9b68505d..3a4d0b8f1f 100644 --- a/langchain4j-jina/revapi.json +++ b/langchain4j-jina/revapi.json @@ -34,6 +34,16 @@ "new": "method retrofit2.Call dev.langchain4j.model.jina.internal.api.JinaApi::embedMultimodal(dev.langchain4j.model.jina.internal.api.JinaMultimodalEmbeddingRequest, java.lang.String)", "justification": "JinaApi is an internal Retrofit API interface (internal.api package), not part of the public API" }, + { + "code": "java.class.removed", + "old": "interface dev.langchain4j.model.jina.internal.api.JinaApi", + "justification": "Internal Retrofit API interface (internal.api package) removed as part of migrating the HTTP transport to the langchain4j HttpClient abstraction; not part of the public API" + }, + { + "code": "java.class.externalClassExposedInAPI", + "new": "missing-class dev.langchain4j.http.client.HttpClientBuilder", + "justification": "HttpClientBuilder is intentionally exposed by the Jina integration for custom HTTP client configuration" + }, { "code": "java.method.removed", "old": "method dev.langchain4j.model.output.Response> dev.langchain4j.model.jina.JinaEmbeddingModel::embedAll(java.util.List)", diff --git a/langchain4j-jina/src/main/java/dev/langchain4j/model/jina/JinaEmbeddingModel.java b/langchain4j-jina/src/main/java/dev/langchain4j/model/jina/JinaEmbeddingModel.java index e9197ad3c3..de20f38998 100644 --- a/langchain4j-jina/src/main/java/dev/langchain4j/model/jina/JinaEmbeddingModel.java +++ b/langchain4j-jina/src/main/java/dev/langchain4j/model/jina/JinaEmbeddingModel.java @@ -6,6 +6,7 @@ import dev.langchain4j.data.message.ContentType; import dev.langchain4j.data.message.ImageContent; import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.exception.UnsupportedFeatureException; +import dev.langchain4j.http.client.HttpClientBuilder; import dev.langchain4j.model.ModelProvider; import dev.langchain4j.model.embedding.DimensionAwareEmbeddingModel; import dev.langchain4j.model.embedding.EmbeddingModel; @@ -73,6 +74,7 @@ public class JinaEmbeddingModel extends DimensionAwareEmbeddingModel { public JinaEmbeddingModel(JinaEmbeddingModelBuilder builder) { this.client = JinaClient.builder() + .httpClientBuilder(builder.httpClientBuilder) .baseUrl(getOrDefault(builder.baseUrl, DEFAULT_BASE_URL)) .apiKey(builder.apiKey) .timeout(getOrDefault(builder.timeout, ofSeconds(60))) @@ -199,6 +201,7 @@ public class JinaEmbeddingModel extends DimensionAwareEmbeddingModel { private Boolean logRequests; private Boolean logResponses; private Logger logger; + private HttpClientBuilder httpClientBuilder; private List listeners; JinaEmbeddingModelBuilder() { @@ -258,6 +261,18 @@ public class JinaEmbeddingModel extends DimensionAwareEmbeddingModel { return this; } + /** + * Sets a custom HTTP client builder, allowing fine-grained control over the HTTP client + * configuration such as timeouts and proxy settings. + * + * @param httpClientBuilder the HTTP client builder + * @return {@code this} + */ + public JinaEmbeddingModelBuilder httpClientBuilder(HttpClientBuilder httpClientBuilder) { + this.httpClientBuilder = httpClientBuilder; + return this; + } + public JinaEmbeddingModel build() { return new JinaEmbeddingModel(this); } diff --git a/langchain4j-jina/src/main/java/dev/langchain4j/model/jina/JinaScoringModel.java b/langchain4j-jina/src/main/java/dev/langchain4j/model/jina/JinaScoringModel.java index 74031f5050..b2838f058f 100644 --- a/langchain4j-jina/src/main/java/dev/langchain4j/model/jina/JinaScoringModel.java +++ b/langchain4j-jina/src/main/java/dev/langchain4j/model/jina/JinaScoringModel.java @@ -8,6 +8,7 @@ import static java.util.Comparator.comparingInt; import static java.util.stream.Collectors.toList; import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.http.client.HttpClientBuilder; import dev.langchain4j.model.jina.internal.api.JinaRerankingRequest; import dev.langchain4j.model.jina.internal.api.JinaRerankingResponse; import dev.langchain4j.model.jina.internal.client.JinaClient; @@ -52,6 +53,7 @@ public class JinaScoringModel implements ScoringModel { public JinaScoringModel(JinaScoringModelBuilder builder) { this.client = JinaClient.builder() + .httpClientBuilder(builder.httpClientBuilder) .baseUrl(getOrDefault(builder.baseUrl, DEFAULT_BASE_URL)) .apiKey(ensureNotBlank(builder.apiKey, "apiKey")) .timeout(getOrDefault(builder.timeout, ofSeconds(60))) @@ -97,6 +99,7 @@ public class JinaScoringModel implements ScoringModel { private Boolean logRequests; private Boolean logResponses; private Logger logger; + private HttpClientBuilder httpClientBuilder; JinaScoringModelBuilder() {} @@ -144,6 +147,18 @@ public class JinaScoringModel implements ScoringModel { return this; } + /** + * Sets a custom HTTP client builder, allowing fine-grained control over the HTTP client + * configuration such as timeouts and proxy settings. + * + * @param httpClientBuilder the HTTP client builder + * @return {@code this} + */ + public JinaScoringModelBuilder httpClientBuilder(HttpClientBuilder httpClientBuilder) { + this.httpClientBuilder = httpClientBuilder; + return this; + } + public JinaScoringModel build() { return new JinaScoringModel(this); } diff --git a/langchain4j-jina/src/main/java/dev/langchain4j/model/jina/internal/api/JinaApi.java b/langchain4j-jina/src/main/java/dev/langchain4j/model/jina/internal/api/JinaApi.java deleted file mode 100644 index a002aeecda..0000000000 --- a/langchain4j-jina/src/main/java/dev/langchain4j/model/jina/internal/api/JinaApi.java +++ /dev/null @@ -1,25 +0,0 @@ -package dev.langchain4j.model.jina.internal.api; - -import retrofit2.Call; -import retrofit2.http.Body; -import retrofit2.http.Header; -import retrofit2.http.Headers; -import retrofit2.http.POST; - -public interface JinaApi { - - @POST("v1/embeddings") - @Headers({"Content-Type: application/json"}) - Call embed(@Body JinaEmbeddingRequest request, - @Header("Authorization") String authorizationHeader); - - @POST("v1/embeddings") - @Headers({"Content-Type: application/json"}) - Call embedMultimodal(@Body JinaMultimodalEmbeddingRequest request, - @Header("Authorization") String authorizationHeader); - - @POST("rerank") - @Headers({"Content-Type: application/json"}) - Call rerank(@Body JinaRerankingRequest request, - @Header("Authorization") String authorizationHeader); -} diff --git a/langchain4j-jina/src/main/java/dev/langchain4j/model/jina/internal/client/JinaClient.java b/langchain4j-jina/src/main/java/dev/langchain4j/model/jina/internal/client/JinaClient.java index 81589a1c2a..a4f122709a 100644 --- a/langchain4j-jina/src/main/java/dev/langchain4j/model/jina/internal/client/JinaClient.java +++ b/langchain4j-jina/src/main/java/dev/langchain4j/model/jina/internal/client/JinaClient.java @@ -1,53 +1,50 @@ package dev.langchain4j.model.jina.internal.client; -import static com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT; +import static dev.langchain4j.http.client.HttpMethod.POST; +import static dev.langchain4j.internal.Utils.ensureTrailingForwardSlash; +import static dev.langchain4j.internal.Utils.getOrDefault; import static dev.langchain4j.internal.ValidationUtils.ensureNotBlank; +import static dev.langchain4j.model.jina.internal.client.JinaJsonUtils.fromJson; +import static dev.langchain4j.model.jina.internal.client.JinaJsonUtils.toJson; -import com.fasterxml.jackson.databind.ObjectMapper; -import dev.langchain4j.internal.Utils; -import dev.langchain4j.model.jina.internal.api.JinaApi; +import dev.langchain4j.http.client.HttpClient; +import dev.langchain4j.http.client.HttpClientBuilder; +import dev.langchain4j.http.client.HttpClientBuilderLoader; +import dev.langchain4j.http.client.HttpRequest; +import dev.langchain4j.http.client.SuccessfulHttpResponse; +import dev.langchain4j.http.client.log.LoggingHttpClient; import dev.langchain4j.model.jina.internal.api.JinaEmbeddingRequest; import dev.langchain4j.model.jina.internal.api.JinaEmbeddingResponse; +import dev.langchain4j.model.jina.internal.api.JinaMultimodalEmbeddingRequest; import dev.langchain4j.model.jina.internal.api.JinaRerankingRequest; import dev.langchain4j.model.jina.internal.api.JinaRerankingResponse; -import java.io.IOException; import java.time.Duration; -import okhttp3.OkHttpClient; import org.slf4j.Logger; -import retrofit2.Retrofit; -import retrofit2.converter.jackson.JacksonConverterFactory; public class JinaClient { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().enable(INDENT_OUTPUT); - - private final JinaApi jinaApi; + private final HttpClient httpClient; + private final String baseUrl; private final String authorizationHeader; - JinaClient( - String baseUrl, String apiKey, Duration timeout, boolean logRequests, boolean logResponses, Logger logger) { + JinaClient(JinaClientBuilder builder) { + HttpClientBuilder httpClientBuilder = + getOrDefault(builder.httpClientBuilder, HttpClientBuilderLoader::loadHttpClientBuilder); - OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder() - .callTimeout(timeout) - .connectTimeout(timeout) - .readTimeout(timeout) - .writeTimeout(timeout); - - if (logRequests) { - okHttpClientBuilder.addInterceptor(new RequestLoggingInterceptor(logger)); - } - if (logResponses) { - okHttpClientBuilder.addInterceptor(new ResponseLoggingInterceptor(logger)); - } - - Retrofit retrofit = new Retrofit.Builder() - .baseUrl(Utils.ensureTrailingForwardSlash(baseUrl)) - .client(okHttpClientBuilder.build()) - .addConverterFactory(JacksonConverterFactory.create(OBJECT_MAPPER)) + HttpClient httpClient = httpClientBuilder + .connectTimeout(builder.timeout) + .readTimeout(builder.timeout) .build(); - this.jinaApi = retrofit.create(JinaApi.class); - this.authorizationHeader = "Bearer " + ensureNotBlank(apiKey, "apiKey"); + if (builder.logRequests || builder.logResponses) { + this.httpClient = + new LoggingHttpClient(httpClient, builder.logRequests, builder.logResponses, builder.logger); + } else { + this.httpClient = httpClient; + } + + this.baseUrl = ensureTrailingForwardSlash(builder.baseUrl); + this.authorizationHeader = "Bearer " + ensureNotBlank(builder.apiKey, "apiKey"); } public static JinaClientBuilder builder() { @@ -55,54 +52,29 @@ public class JinaClient { } public JinaEmbeddingResponse embed(JinaEmbeddingRequest request) { - try { - retrofit2.Response retrofitResponse = - jinaApi.embed(request, authorizationHeader).execute(); - if (retrofitResponse.isSuccessful()) { - return retrofitResponse.body(); - } else { - throw toException(retrofitResponse); - } - } catch (IOException e) { - throw new RuntimeException(e); - } + return post("v1/embeddings", request, JinaEmbeddingResponse.class); } - public JinaEmbeddingResponse embedMultimodal( - dev.langchain4j.model.jina.internal.api.JinaMultimodalEmbeddingRequest request) { - try { - retrofit2.Response retrofitResponse = - jinaApi.embedMultimodal(request, authorizationHeader).execute(); - if (retrofitResponse.isSuccessful()) { - return retrofitResponse.body(); - } else { - throw toException(retrofitResponse); - } - } catch (IOException e) { - throw new RuntimeException(e); - } + public JinaEmbeddingResponse embedMultimodal(JinaMultimodalEmbeddingRequest request) { + return post("v1/embeddings", request, JinaEmbeddingResponse.class); } public JinaRerankingResponse rerank(JinaRerankingRequest request) { - try { - retrofit2.Response retrofitResponse = - jinaApi.rerank(request, authorizationHeader).execute(); - - if (retrofitResponse.isSuccessful()) { - return retrofitResponse.body(); - } else { - throw toException(retrofitResponse); - } - } catch (IOException e) { - throw new RuntimeException(e); - } + return post("rerank", request, JinaRerankingResponse.class); } - private static RuntimeException toException(retrofit2.Response response) throws IOException { - int code = response.code(); - String body = response.errorBody().string(); - String errorMessage = String.format("status code: %s; body: %s", code, body); - return new RuntimeException(errorMessage); + private T post(String path, Object request, Class responseType) { + HttpRequest httpRequest = HttpRequest.builder() + .method(POST) + .url(baseUrl + path) + .addHeader("Content-Type", "application/json") + .addHeader("Authorization", authorizationHeader) + .body(toJson(request)) + .build(); + + SuccessfulHttpResponse response = httpClient.execute(httpRequest); + + return fromJson(response.body(), responseType); } public static class JinaClientBuilder { @@ -112,6 +84,7 @@ public class JinaClient { private boolean logRequests; private boolean logResponses; private Logger logger; + private HttpClientBuilder httpClientBuilder; JinaClientBuilder() {} @@ -145,9 +118,13 @@ public class JinaClient { return this; } + public JinaClientBuilder httpClientBuilder(HttpClientBuilder httpClientBuilder) { + this.httpClientBuilder = httpClientBuilder; + return this; + } + public JinaClient build() { - return new JinaClient( - this.baseUrl, this.apiKey, this.timeout, this.logRequests, this.logResponses, this.logger); + return new JinaClient(this); } public String toString() { diff --git a/langchain4j-jina/src/main/java/dev/langchain4j/model/jina/internal/client/JinaJsonUtils.java b/langchain4j-jina/src/main/java/dev/langchain4j/model/jina/internal/client/JinaJsonUtils.java new file mode 100644 index 0000000000..d4fb5f2fa6 --- /dev/null +++ b/langchain4j-jina/src/main/java/dev/langchain4j/model/jina/internal/client/JinaJsonUtils.java @@ -0,0 +1,31 @@ +package dev.langchain4j.model.jina.internal.client; + +import static com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +class JinaJsonUtils { + + private JinaJsonUtils() throws InstantiationException { + throw new InstantiationException("Can't instantiate this utility class."); + } + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().enable(INDENT_OUTPUT); + + static String toJson(Object object) { + try { + return OBJECT_MAPPER.writeValueAsString(object); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + static T fromJson(String jsonStr, Class clazz) { + try { + return OBJECT_MAPPER.readValue(jsonStr, clazz); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } +} diff --git a/langchain4j-jina/src/main/java/dev/langchain4j/model/jina/internal/client/RequestLoggingInterceptor.java b/langchain4j-jina/src/main/java/dev/langchain4j/model/jina/internal/client/RequestLoggingInterceptor.java deleted file mode 100644 index d8d6eae26f..0000000000 --- a/langchain4j-jina/src/main/java/dev/langchain4j/model/jina/internal/client/RequestLoggingInterceptor.java +++ /dev/null @@ -1,89 +0,0 @@ -package dev.langchain4j.model.jina.internal.client; - -import okhttp3.Headers; -import okhttp3.Interceptor; -import okhttp3.Request; -import okhttp3.Response; -import okio.Buffer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -import static dev.langchain4j.internal.Utils.getOrDefault; -import static java.util.stream.StreamSupport.stream; - -class RequestLoggingInterceptor implements Interceptor { - - private static final Logger DEFAULT_LOG = LoggerFactory.getLogger(RequestLoggingInterceptor.class); - - private static final Pattern BEARER_PATTERN = Pattern.compile("(Bearer\\s)(\\w{2})(\\w+)(\\w{2})"); - - private final Logger log; - - RequestLoggingInterceptor(Logger logger) { - this.log = getOrDefault(logger, DEFAULT_LOG); - } - - public Response intercept(Interceptor.Chain chain) throws IOException { - Request request = chain.request(); - log(request); - return chain.proceed(request); - } - - private void log(Request request) { - log.debug( - "Request:\n" + - "- method: {}\n" + - "- url: {}\n" + - "- headers: {}\n" + - "- body: {}", - request.method(), - request.url(), - inOneLine(request.headers()), - getBody(request) - ); - } - - static String inOneLine(Headers headers) { - return stream(headers.spliterator(), false) - .map((header) -> { - String headerKey = header.component1(); - String headerValue = header.component2(); - if (headerKey.equals("Authorization")) { - headerValue = maskAuthorizationHeaderValue(headerValue); - } - return String.format("[%s: %s]", headerKey, headerValue); - }).collect(Collectors.joining(", ")); - } - - private static String maskAuthorizationHeaderValue(String authorizationHeaderValue) { - try { - Matcher matcher = BEARER_PATTERN.matcher(authorizationHeaderValue); - StringBuffer sb = new StringBuffer(); - - while (matcher.find()) { - matcher.appendReplacement(sb, matcher.group(1) + matcher.group(2) + "..." + matcher.group(4)); - } - - matcher.appendTail(sb); - return sb.toString(); - } catch (Exception e) { - return "[failed to mask the API key]"; - } - } - - private String getBody(Request request) { - try { - Buffer buffer = new Buffer(); - request.body().writeTo(buffer); - return buffer.readUtf8(); - } catch (Exception e) { - log.warn("Exception happened while reading request body", e); - return "[Exception happened while reading request body. Check logs for more details.]"; - } - } -} diff --git a/langchain4j-jina/src/main/java/dev/langchain4j/model/jina/internal/client/ResponseLoggingInterceptor.java b/langchain4j-jina/src/main/java/dev/langchain4j/model/jina/internal/client/ResponseLoggingInterceptor.java deleted file mode 100644 index 2288d3821a..0000000000 --- a/langchain4j-jina/src/main/java/dev/langchain4j/model/jina/internal/client/ResponseLoggingInterceptor.java +++ /dev/null @@ -1,50 +0,0 @@ -package dev.langchain4j.model.jina.internal.client; - -import okhttp3.Interceptor; -import okhttp3.Request; -import okhttp3.Response; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; - -import static dev.langchain4j.internal.Utils.getOrDefault; - -class ResponseLoggingInterceptor implements Interceptor { - - private static final Logger DEFAULT_LOG = LoggerFactory.getLogger(ResponseLoggingInterceptor.class); - - private final Logger log; - - ResponseLoggingInterceptor(Logger logger) { - this.log = getOrDefault(logger, DEFAULT_LOG); - } - - public Response intercept(Interceptor.Chain chain) throws IOException { - Request request = chain.request(); - Response response = chain.proceed(request); - log(response); - return response; - } - - void log(Response response) { - log.debug( - "Response:\n" + - "- status code: {}\n" + - "- headers: {}\n" + - "- body: {}", - response.code(), - RequestLoggingInterceptor.inOneLine(response.headers()), - getBody(response) - ); - } - - private String getBody(Response response) { - try { - return response.peekBody(Long.MAX_VALUE).string(); - } catch (IOException e) { - log.warn("Failed to log response", e); - return "[failed to log response]"; - } - } -} diff --git a/langchain4j-nomic/pom.xml b/langchain4j-nomic/pom.xml index 5fa4df1a2a..d2687bdd24 100644 --- a/langchain4j-nomic/pom.xml +++ b/langchain4j-nomic/pom.xml @@ -20,18 +20,16 @@ - com.squareup.retrofit2 - retrofit + dev.langchain4j + langchain4j-http-client + 1.18.0-SNAPSHOT - com.squareup.okhttp3 - okhttp - - - - com.squareup.retrofit2 - converter-jackson + dev.langchain4j + langchain4j-http-client-jdk + 1.18.0-SNAPSHOT + runtime diff --git a/langchain4j-nomic/revapi.json b/langchain4j-nomic/revapi.json new file mode 100644 index 0000000000..c3bc042f7b --- /dev/null +++ b/langchain4j-nomic/revapi.json @@ -0,0 +1,15 @@ +[ + { + "extension": "revapi.differences", + "configuration": { + "ignore": true, + "differences": [ + { + "code": "java.class.externalClassExposedInAPI", + "new": "missing-class dev.langchain4j.http.client.HttpClientBuilder", + "justification": "HttpClientBuilder is intentionally exposed by the Nomic integration for custom HTTP client configuration" + } + ] + } + } +] diff --git a/langchain4j-nomic/src/main/java/dev/langchain4j/model/nomic/NomicApi.java b/langchain4j-nomic/src/main/java/dev/langchain4j/model/nomic/NomicApi.java deleted file mode 100644 index 1425cff11e..0000000000 --- a/langchain4j-nomic/src/main/java/dev/langchain4j/model/nomic/NomicApi.java +++ /dev/null @@ -1,14 +0,0 @@ -package dev.langchain4j.model.nomic; - -import retrofit2.Call; -import retrofit2.http.Body; -import retrofit2.http.Header; -import retrofit2.http.Headers; -import retrofit2.http.POST; - -interface NomicApi { - - @POST("embedding/text") - @Headers({"Content-Type: application/json"}) - Call embed(@Body EmbeddingRequest request, @Header("Authorization") String authorizationHeader); -} diff --git a/langchain4j-nomic/src/main/java/dev/langchain4j/model/nomic/NomicClient.java b/langchain4j-nomic/src/main/java/dev/langchain4j/model/nomic/NomicClient.java index e26a57014b..844d663294 100644 --- a/langchain4j-nomic/src/main/java/dev/langchain4j/model/nomic/NomicClient.java +++ b/langchain4j-nomic/src/main/java/dev/langchain4j/model/nomic/NomicClient.java @@ -1,55 +1,46 @@ package dev.langchain4j.model.nomic; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.PropertyNamingStrategies; -import com.fasterxml.jackson.databind.SerializationFeature; -import dev.langchain4j.internal.Utils; -import okhttp3.OkHttpClient; -import org.slf4j.Logger; -import retrofit2.Retrofit; -import retrofit2.converter.jackson.JacksonConverterFactory; - -import java.io.IOException; -import java.time.Duration; - +import static dev.langchain4j.http.client.HttpMethod.POST; +import static dev.langchain4j.internal.Utils.ensureTrailingForwardSlash; +import static dev.langchain4j.internal.Utils.getOrDefault; import static dev.langchain4j.internal.ValidationUtils.ensureNotBlank; +import static dev.langchain4j.model.nomic.NomicJsonUtils.fromJson; +import static dev.langchain4j.model.nomic.NomicJsonUtils.toJson; + +import dev.langchain4j.http.client.HttpClient; +import dev.langchain4j.http.client.HttpClientBuilder; +import dev.langchain4j.http.client.HttpClientBuilderLoader; +import dev.langchain4j.http.client.HttpRequest; +import dev.langchain4j.http.client.SuccessfulHttpResponse; +import dev.langchain4j.http.client.log.LoggingHttpClient; +import java.time.Duration; +import org.slf4j.Logger; class NomicClient { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() - .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE) - .enable(SerializationFeature.INDENT_OUTPUT) - .setSerializationInclusion(JsonInclude.Include.NON_NULL) - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - - private final NomicApi nomicApi; + private final HttpClient httpClient; + private final String baseUrl; private final String authorizationHeader; - NomicClient(String baseUrl, String apiKey, Duration timeout, Boolean logRequests, Boolean logResponses, Logger logger) { + NomicClient(NomicClientBuilder builder) { + HttpClientBuilder httpClientBuilder = + getOrDefault(builder.httpClientBuilder, HttpClientBuilderLoader::loadHttpClientBuilder); - OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder() - .callTimeout(timeout) - .connectTimeout(timeout) - .readTimeout(timeout) - .writeTimeout(timeout); - - if (logRequests) { - okHttpClientBuilder.addInterceptor(new RequestLoggingInterceptor(logger)); - } - if (logResponses) { - okHttpClientBuilder.addInterceptor(new ResponseLoggingInterceptor(logger)); - } - - Retrofit retrofit = new Retrofit.Builder() - .baseUrl(Utils.ensureTrailingForwardSlash(baseUrl)) - .client(okHttpClientBuilder.build()) - .addConverterFactory(JacksonConverterFactory.create(OBJECT_MAPPER)) + HttpClient httpClient = httpClientBuilder + .connectTimeout(builder.timeout) + .readTimeout(builder.timeout) .build(); - this.nomicApi = retrofit.create(NomicApi.class); - this.authorizationHeader = "Bearer " + ensureNotBlank(apiKey, "apiKey"); + if (builder.logRequests != null && builder.logRequests + || builder.logResponses != null && builder.logResponses) { + this.httpClient = + new LoggingHttpClient(httpClient, builder.logRequests, builder.logResponses, builder.logger); + } else { + this.httpClient = httpClient; + } + + this.baseUrl = ensureTrailingForwardSlash(builder.baseUrl); + this.authorizationHeader = "Bearer " + ensureNotBlank(builder.apiKey, "apiKey"); } public static NomicClientBuilder builder() { @@ -57,25 +48,17 @@ class NomicClient { } public EmbeddingResponse embed(EmbeddingRequest request) { - try { - retrofit2.Response retrofitResponse - = nomicApi.embed(request, authorizationHeader).execute(); + HttpRequest httpRequest = HttpRequest.builder() + .method(POST) + .url(baseUrl + "embedding/text") + .addHeader("Content-Type", "application/json") + .addHeader("Authorization", authorizationHeader) + .body(toJson(request)) + .build(); - if (retrofitResponse.isSuccessful()) { - return retrofitResponse.body(); - } else { - throw toException(retrofitResponse); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } + SuccessfulHttpResponse response = httpClient.execute(httpRequest); - private static RuntimeException toException(retrofit2.Response response) throws IOException { - int code = response.code(); - String body = response.errorBody().string(); - String errorMessage = String.format("status code: %s; body: %s", code, body); - return new RuntimeException(errorMessage); + return fromJson(response.body(), EmbeddingResponse.class); } public static class NomicClientBuilder { @@ -85,6 +68,7 @@ class NomicClient { private Boolean logRequests; private Boolean logResponses; private Logger logger; + private HttpClientBuilder httpClientBuilder; NomicClientBuilder() { } @@ -119,8 +103,13 @@ class NomicClient { return this; } + public NomicClientBuilder httpClientBuilder(HttpClientBuilder httpClientBuilder) { + this.httpClientBuilder = httpClientBuilder; + return this; + } + public NomicClient build() { - return new NomicClient(this.baseUrl, this.apiKey, this.timeout, this.logRequests, this.logResponses, this.logger); + return new NomicClient(this); } public String toString() { diff --git a/langchain4j-nomic/src/main/java/dev/langchain4j/model/nomic/NomicEmbeddingModel.java b/langchain4j-nomic/src/main/java/dev/langchain4j/model/nomic/NomicEmbeddingModel.java index 427177ff7d..002f35c969 100644 --- a/langchain4j-nomic/src/main/java/dev/langchain4j/model/nomic/NomicEmbeddingModel.java +++ b/langchain4j-nomic/src/main/java/dev/langchain4j/model/nomic/NomicEmbeddingModel.java @@ -8,6 +8,7 @@ import static java.util.stream.Collectors.toList; import dev.langchain4j.data.embedding.Embedding; import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.http.client.HttpClientBuilder; import dev.langchain4j.model.embedding.DimensionAwareEmbeddingModel; import dev.langchain4j.model.output.Response; import dev.langchain4j.model.output.TokenUsage; @@ -56,11 +57,13 @@ public class NomicEmbeddingModel extends DimensionAwareEmbeddingModel { public NomicEmbeddingModel(NomicEmbeddingModelBuilder builder) { this.client = NomicClient.builder() + .httpClientBuilder(builder.httpClientBuilder) .baseUrl(getOrDefault(builder.baseUrl, DEFAULT_BASE_URL)) .apiKey(ensureNotBlank(builder.apiKey, "apiKey")) .timeout(getOrDefault(builder.timeout, ofSeconds(60))) .logRequests(getOrDefault(builder.logRequests, false)) .logResponses(getOrDefault(builder.logResponses, false)) + .logger(builder.logger) .build(); this.modelName = ensureNotBlank(builder.modelName, "modelName"); this.taskType = builder.taskType; @@ -130,6 +133,7 @@ public class NomicEmbeddingModel extends DimensionAwareEmbeddingModel { private Boolean logRequests; private Boolean logResponses; private Logger logger; + private HttpClientBuilder httpClientBuilder; NomicEmbeddingModelBuilder() {} @@ -187,6 +191,18 @@ public class NomicEmbeddingModel extends DimensionAwareEmbeddingModel { return this; } + /** + * Sets a custom HTTP client builder, allowing fine-grained control over the HTTP client + * configuration such as timeouts and proxy settings. + * + * @param httpClientBuilder the HTTP client builder + * @return {@code this} + */ + public NomicEmbeddingModelBuilder httpClientBuilder(HttpClientBuilder httpClientBuilder) { + this.httpClientBuilder = httpClientBuilder; + return this; + } + public NomicEmbeddingModel build() { return new NomicEmbeddingModel(this); } diff --git a/langchain4j-nomic/src/main/java/dev/langchain4j/model/nomic/NomicJsonUtils.java b/langchain4j-nomic/src/main/java/dev/langchain4j/model/nomic/NomicJsonUtils.java new file mode 100644 index 0000000000..2f98323a1f --- /dev/null +++ b/langchain4j-nomic/src/main/java/dev/langchain4j/model/nomic/NomicJsonUtils.java @@ -0,0 +1,37 @@ +package dev.langchain4j.model.nomic; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.SerializationFeature; + +class NomicJsonUtils { + + private NomicJsonUtils() throws InstantiationException { + throw new InstantiationException("Can't instantiate this utility class."); + } + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() + .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE) + .enable(SerializationFeature.INDENT_OUTPUT) + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + static String toJson(Object object) { + try { + return OBJECT_MAPPER.writeValueAsString(object); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + static T fromJson(String jsonStr, Class clazz) { + try { + return OBJECT_MAPPER.readValue(jsonStr, clazz); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } +} diff --git a/langchain4j-nomic/src/main/java/dev/langchain4j/model/nomic/RequestLoggingInterceptor.java b/langchain4j-nomic/src/main/java/dev/langchain4j/model/nomic/RequestLoggingInterceptor.java deleted file mode 100644 index bf4d3d325d..0000000000 --- a/langchain4j-nomic/src/main/java/dev/langchain4j/model/nomic/RequestLoggingInterceptor.java +++ /dev/null @@ -1,89 +0,0 @@ -package dev.langchain4j.model.nomic; - -import okhttp3.Headers; -import okhttp3.Interceptor; -import okhttp3.Request; -import okhttp3.Response; -import okio.Buffer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -import static dev.langchain4j.internal.Utils.getOrDefault; -import static java.util.stream.StreamSupport.stream; - -class RequestLoggingInterceptor implements Interceptor { - - private static final Logger DEFAULT_LOG = LoggerFactory.getLogger(RequestLoggingInterceptor.class); - - private static final Pattern BEARER_PATTERN = Pattern.compile("(Bearer\\s*)([\\w-]{5})([\\w-]+)([\\w-]{2})"); - - private final Logger log; - - RequestLoggingInterceptor(Logger logger) { - this.log = getOrDefault(logger, DEFAULT_LOG); - } - - public Response intercept(Interceptor.Chain chain) throws IOException { - Request request = chain.request(); - log(request); - return chain.proceed(request); - } - - private void log(Request request) { - log.debug( - "Request:\n" + - "- method: {}\n" + - "- url: {}\n" + - "- headers: {}\n" + - "- body: {}", - request.method(), - request.url(), - inOneLine(request.headers()), - getBody(request) - ); - } - - static String inOneLine(Headers headers) { - return stream(headers.spliterator(), false) - .map((header) -> { - String headerKey = header.component1(); - String headerValue = header.component2(); - if (headerKey.equals("Authorization")) { - headerValue = maskAuthorizationHeaderValue(headerValue); - } - return String.format("[%s: %s]", headerKey, headerValue); - }).collect(Collectors.joining(", ")); - } - - private static String maskAuthorizationHeaderValue(String authorizationHeaderValue) { - try { - Matcher matcher = BEARER_PATTERN.matcher(authorizationHeaderValue); - StringBuffer sb = new StringBuffer(); - - while (matcher.find()) { - matcher.appendReplacement(sb, matcher.group(1) + matcher.group(2) + "..." + matcher.group(4)); - } - - matcher.appendTail(sb); - return sb.toString(); - } catch (Exception e) { - return "[failed to mask the API key]"; - } - } - - private String getBody(Request request) { - try { - Buffer buffer = new Buffer(); - request.body().writeTo(buffer); - return buffer.readUtf8(); - } catch (Exception e) { - log.warn("Exception happened while reading request body", e); - return "[Exception happened while reading request body. Check logs for more details.]"; - } - } -} diff --git a/langchain4j-nomic/src/main/java/dev/langchain4j/model/nomic/ResponseLoggingInterceptor.java b/langchain4j-nomic/src/main/java/dev/langchain4j/model/nomic/ResponseLoggingInterceptor.java deleted file mode 100644 index b182befcee..0000000000 --- a/langchain4j-nomic/src/main/java/dev/langchain4j/model/nomic/ResponseLoggingInterceptor.java +++ /dev/null @@ -1,51 +0,0 @@ -package dev.langchain4j.model.nomic; - -import okhttp3.Interceptor; -import okhttp3.Request; -import okhttp3.Response; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; - -import static dev.langchain4j.internal.Utils.getOrDefault; -import static dev.langchain4j.model.nomic.RequestLoggingInterceptor.inOneLine; - -class ResponseLoggingInterceptor implements Interceptor { - - private static final Logger DEFAULT_LOG = LoggerFactory.getLogger(ResponseLoggingInterceptor.class); - - private final Logger log; - - ResponseLoggingInterceptor(Logger logger) { - this.log = getOrDefault(logger, DEFAULT_LOG); - } - - public Response intercept(Interceptor.Chain chain) throws IOException { - Request request = chain.request(); - Response response = chain.proceed(request); - log(response); - return response; - } - - void log(Response response) { - log.debug( - "Response:\n" + - "- status code: {}\n" + - "- headers: {}\n" + - "- body: {}", - response.code(), - inOneLine(response.headers()), - getBody(response) - ); - } - - private String getBody(Response response) { - try { - return response.peekBody(Long.MAX_VALUE).string(); - } catch (IOException e) { - log.warn("Failed to log response", e); - return "[failed to log response]"; - } - } -} diff --git a/langchain4j-ovh-ai/pom.xml b/langchain4j-ovh-ai/pom.xml index a1544fa12f..49e54fcc6b 100644 --- a/langchain4j-ovh-ai/pom.xml +++ b/langchain4j-ovh-ai/pom.xml @@ -25,13 +25,16 @@ - com.squareup.retrofit2 - retrofit + dev.langchain4j + langchain4j-http-client + 1.18.0-SNAPSHOT - com.squareup.retrofit2 - converter-jackson + dev.langchain4j + langchain4j-http-client-jdk + 1.18.0-SNAPSHOT + runtime diff --git a/langchain4j-ovh-ai/revapi.json b/langchain4j-ovh-ai/revapi.json new file mode 100644 index 0000000000..2fdce93bce --- /dev/null +++ b/langchain4j-ovh-ai/revapi.json @@ -0,0 +1,15 @@ +[ + { + "extension": "revapi.differences", + "configuration": { + "ignore": true, + "differences": [ + { + "code": "java.class.removed", + "old": "interface dev.langchain4j.model.ovhai.internal.api.OvhAiApi", + "justification": "Internal Retrofit API interface (internal.api package) removed as part of migrating the HTTP transport to the langchain4j HttpClient abstraction; not part of the public API" + } + ] + } + } +] diff --git a/langchain4j-ovh-ai/src/main/java/dev/langchain4j/model/ovhai/internal/api/OvhAiApi.java b/langchain4j-ovh-ai/src/main/java/dev/langchain4j/model/ovhai/internal/api/OvhAiApi.java deleted file mode 100644 index 0328de1764..0000000000 --- a/langchain4j-ovh-ai/src/main/java/dev/langchain4j/model/ovhai/internal/api/OvhAiApi.java +++ /dev/null @@ -1,20 +0,0 @@ -package dev.langchain4j.model.ovhai.internal.api; - -import retrofit2.Call; -import retrofit2.http.Body; -import retrofit2.http.Header; -import retrofit2.http.Headers; -import retrofit2.http.POST; - -import java.util.List; - -/** - * @deprecated Do not use anymore, use {@code langchain4j-open-ai} module instead - */ -@Deprecated(forRemoval = true, since = "1.14.0") -public interface OvhAiApi { - - @POST("api/batch_text2vec") - @Headers({"Content-Type: application/json"}) - Call> embed(@Body EmbeddingRequest request, @Header("Authorization") String authorizationHeader); -} diff --git a/langchain4j-ovh-ai/src/main/java/dev/langchain4j/model/ovhai/internal/client/DefaultOvhAiClient.java b/langchain4j-ovh-ai/src/main/java/dev/langchain4j/model/ovhai/internal/client/DefaultOvhAiClient.java index 583de8fee4..53801e0053 100644 --- a/langchain4j-ovh-ai/src/main/java/dev/langchain4j/model/ovhai/internal/client/DefaultOvhAiClient.java +++ b/langchain4j-ovh-ai/src/main/java/dev/langchain4j/model/ovhai/internal/client/DefaultOvhAiClient.java @@ -1,19 +1,20 @@ package dev.langchain4j.model.ovhai.internal.client; -import com.fasterxml.jackson.databind.ObjectMapper; -import dev.langchain4j.internal.Utils; +import static dev.langchain4j.http.client.HttpMethod.POST; +import static dev.langchain4j.internal.Utils.ensureTrailingForwardSlash; +import static dev.langchain4j.internal.ValidationUtils.ensureNotBlank; +import static dev.langchain4j.model.ovhai.internal.client.OvhAiJsonUtils.fromJson; +import static dev.langchain4j.model.ovhai.internal.client.OvhAiJsonUtils.toJson; + +import dev.langchain4j.http.client.HttpClient; +import dev.langchain4j.http.client.HttpClientBuilder; +import dev.langchain4j.http.client.HttpClientBuilderLoader; +import dev.langchain4j.http.client.HttpRequest; +import dev.langchain4j.http.client.SuccessfulHttpResponse; +import dev.langchain4j.http.client.log.LoggingHttpClient; import dev.langchain4j.model.ovhai.internal.api.EmbeddingRequest; import dev.langchain4j.model.ovhai.internal.api.EmbeddingResponse; -import dev.langchain4j.model.ovhai.internal.api.OvhAiApi; -import okhttp3.OkHttpClient; -import retrofit2.Retrofit; -import retrofit2.converter.jackson.JacksonConverterFactory; - -import java.io.IOException; -import java.util.List; - -import static com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT; -import static dev.langchain4j.internal.ValidationUtils.ensureNotBlank; +import java.util.Arrays; /** * @deprecated Do not use anymore, use {@code langchain4j-open-ai} module instead @@ -21,13 +22,8 @@ import static dev.langchain4j.internal.ValidationUtils.ensureNotBlank; @Deprecated(forRemoval = true, since = "1.14.0") public class DefaultOvhAiClient extends OvhAiClient { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().enable(INDENT_OUTPUT); - - private final OkHttpClient okHttpClient; - - private final String apiKey; - private final boolean logResponses; - private final OvhAiApi ovhAiApi; + private final HttpClient httpClient; + private final String baseUrl; private final String authorizationHeader; public static Builder builder() { @@ -44,52 +40,36 @@ public class DefaultOvhAiClient extends OvhAiClient { DefaultOvhAiClient(Builder builder) { ensureNotBlank(builder.apiKey, "%s", "OVHcloud API key must be defined. It can be generated here: https://endpoints.ai.cloud.ovh.net/"); - this.apiKey = builder.apiKey; - this.logResponses = builder.logResponses; + HttpClientBuilder httpClientBuilder = HttpClientBuilderLoader.loadHttpClientBuilder(); + HttpClient httpClient = httpClientBuilder + .connectTimeout(builder.timeout) + .readTimeout(builder.timeout) + .build(); - OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder() - .callTimeout(builder.timeout) - .connectTimeout(builder.timeout) - .readTimeout(builder.timeout) - .writeTimeout(builder.timeout); - - if (builder.logRequests) { - okHttpClientBuilder.addInterceptor(new RequestLoggingInterceptor(builder.logger)); - } - if (logResponses) { - okHttpClientBuilder.addInterceptor(new ResponseLoggingInterceptor(builder.logger)); + if (builder.logRequests != null && builder.logRequests + || builder.logResponses != null && builder.logResponses) { + this.httpClient = + new LoggingHttpClient(httpClient, builder.logRequests, builder.logResponses, builder.logger); + } else { + this.httpClient = httpClient; } - this.okHttpClient = okHttpClientBuilder.build(); - - Retrofit retrofit = new Retrofit.Builder() - .baseUrl(Utils.ensureTrailingForwardSlash(ensureNotBlank(builder.baseUrl, "baseUrl"))) - .client(okHttpClient) - .addConverterFactory(JacksonConverterFactory.create(OBJECT_MAPPER)) - .build(); - - this.ovhAiApi = retrofit.create(OvhAiApi.class); - this.authorizationHeader = "Bearer " + ensureNotBlank(apiKey, "apiKey"); + this.baseUrl = ensureTrailingForwardSlash(ensureNotBlank(builder.baseUrl, "baseUrl")); + this.authorizationHeader = "Bearer " + ensureNotBlank(builder.apiKey, "apiKey"); } public EmbeddingResponse embed(EmbeddingRequest request) { - try { - retrofit2.Response> retrofitResponse = ovhAiApi.embed(request, authorizationHeader).execute(); + HttpRequest httpRequest = HttpRequest.builder() + .method(POST) + .url(baseUrl + "api/batch_text2vec") + .addHeader("Content-Type", "application/json") + .addHeader("Authorization", authorizationHeader) + .body(toJson(request)) + .build(); - if (retrofitResponse.isSuccessful()) { - return new EmbeddingResponse(retrofitResponse.body()); - } else { - throw toException(retrofitResponse); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } + SuccessfulHttpResponse response = httpClient.execute(httpRequest); - private static RuntimeException toException(retrofit2.Response response) throws IOException { - int code = response.code(); - String body = response.errorBody().string(); - String errorMessage = String.format("status code: %s; body: %s", code, body); - return new RuntimeException(errorMessage); + float[][] embeddings = fromJson(response.body(), float[][].class); + return new EmbeddingResponse(Arrays.asList(embeddings)); } } diff --git a/langchain4j-ovh-ai/src/main/java/dev/langchain4j/model/ovhai/internal/client/OvhAiJsonUtils.java b/langchain4j-ovh-ai/src/main/java/dev/langchain4j/model/ovhai/internal/client/OvhAiJsonUtils.java new file mode 100644 index 0000000000..61f566488c --- /dev/null +++ b/langchain4j-ovh-ai/src/main/java/dev/langchain4j/model/ovhai/internal/client/OvhAiJsonUtils.java @@ -0,0 +1,31 @@ +package dev.langchain4j.model.ovhai.internal.client; + +import static com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +class OvhAiJsonUtils { + + private OvhAiJsonUtils() throws InstantiationException { + throw new InstantiationException("Can't instantiate this utility class."); + } + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().enable(INDENT_OUTPUT); + + static String toJson(Object object) { + try { + return OBJECT_MAPPER.writeValueAsString(object); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + static T fromJson(String jsonStr, Class clazz) { + try { + return OBJECT_MAPPER.readValue(jsonStr, clazz); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } +} diff --git a/langchain4j-ovh-ai/src/main/java/dev/langchain4j/model/ovhai/internal/client/RequestLoggingInterceptor.java b/langchain4j-ovh-ai/src/main/java/dev/langchain4j/model/ovhai/internal/client/RequestLoggingInterceptor.java deleted file mode 100644 index 9558374dc7..0000000000 --- a/langchain4j-ovh-ai/src/main/java/dev/langchain4j/model/ovhai/internal/client/RequestLoggingInterceptor.java +++ /dev/null @@ -1,93 +0,0 @@ -package dev.langchain4j.model.ovhai.internal.client; - -import okhttp3.Headers; -import okhttp3.Interceptor; -import okhttp3.Request; -import okhttp3.Response; -import okio.Buffer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -import static dev.langchain4j.internal.Utils.getOrDefault; -import static java.util.stream.StreamSupport.stream; - -/** - * @deprecated Do not use anymore, use {@code langchain4j-open-ai} module instead - */ -@Deprecated(forRemoval = true, since = "1.14.0") -class RequestLoggingInterceptor implements Interceptor { - - private static final Logger DEFAULT_LOG = LoggerFactory.getLogger(RequestLoggingInterceptor.class); - - private static final Pattern BEARER_PATTERN = Pattern.compile("(Bearer\\s*)([\\w-]{5})([\\w-]+)([\\w-]{2})"); - - private final Logger log; - - RequestLoggingInterceptor(Logger logger) { - this.log = getOrDefault(logger, DEFAULT_LOG); - } - - public Response intercept(Interceptor.Chain chain) throws IOException { - Request request = chain.request(); - log(request); - return chain.proceed(request); - } - - private void log(Request request) { - log.debug( - "Request:\n" + - "- method: {}\n" + - "- url: {}\n" + - "- headers: {}\n" + - "- body: {}", - request.method(), - request.url(), - inOneLine(request.headers()), - getBody(request) - ); - } - - static String inOneLine(Headers headers) { - return stream(headers.spliterator(), false) - .map((header) -> { - String headerKey = header.component1(); - String headerValue = header.component2(); - if (headerKey.equals("Authorization")) { - headerValue = maskAuthorizationHeaderValue(headerValue); - } - return String.format("[%s: %s]", headerKey, headerValue); - }).collect(Collectors.joining(", ")); - } - - private static String maskAuthorizationHeaderValue(String authorizationHeaderValue) { - try { - Matcher matcher = BEARER_PATTERN.matcher(authorizationHeaderValue); - StringBuffer sb = new StringBuffer(); - - while (matcher.find()) { - matcher.appendReplacement(sb, matcher.group(1) + matcher.group(2) + "..." + matcher.group(4)); - } - - matcher.appendTail(sb); - return sb.toString(); - } catch (Exception e) { - return "[failed to mask the API key]"; - } - } - - private String getBody(Request request) { - try { - Buffer buffer = new Buffer(); - request.body().writeTo(buffer); - return buffer.readUtf8(); - } catch (Exception e) { - log.warn("Exception happened while reading request body", e); - return "[Exception happened while reading request body. Check logs for more details.]"; - } - } -} diff --git a/langchain4j-ovh-ai/src/main/java/dev/langchain4j/model/ovhai/internal/client/ResponseLoggingInterceptor.java b/langchain4j-ovh-ai/src/main/java/dev/langchain4j/model/ovhai/internal/client/ResponseLoggingInterceptor.java deleted file mode 100644 index 595cb30e83..0000000000 --- a/langchain4j-ovh-ai/src/main/java/dev/langchain4j/model/ovhai/internal/client/ResponseLoggingInterceptor.java +++ /dev/null @@ -1,54 +0,0 @@ -package dev.langchain4j.model.ovhai.internal.client; - -import okhttp3.Interceptor; -import okhttp3.Request; -import okhttp3.Response; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import static dev.langchain4j.internal.Utils.getOrDefault; -import static dev.langchain4j.model.ovhai.internal.client.RequestLoggingInterceptor.inOneLine; -import java.io.IOException; - -/** - * @deprecated Do not use anymore, use {@code langchain4j-open-ai} module instead - */ -@Deprecated(forRemoval = true, since = "1.14.0") -class ResponseLoggingInterceptor implements Interceptor { - - private static final Logger DEFAULT_LOG = LoggerFactory.getLogger(ResponseLoggingInterceptor.class); - - private final Logger log; - - ResponseLoggingInterceptor(Logger logger) { - this.log = getOrDefault(logger, DEFAULT_LOG); - } - - public Response intercept(Interceptor.Chain chain) throws IOException { - Request request = chain.request(); - Response response = chain.proceed(request); - log(response); - return response; - } - - void log(Response response) { - log.debug( - "Response:\n" + - "- status code: {}\n" + - "- headers: {}\n" + - "- body: {}", - response.code(), - inOneLine(response.headers()), - getBody(response) - ); - } - - private String getBody(Response response) { - try { - return response.peekBody(Long.MAX_VALUE).string(); - } catch (IOException e) { - log.warn("Failed to log response", e); - return "[failed to log response]"; - } - } -}