apply builder pattern to OllamaApi
Signed-off-by: jonghoon park <dev@jonghoonpark.com>
This commit is contained in:
committed by
Ilayaperumal Gopinathan
parent
53406c1dd3
commit
d48f4f63db
@@ -61,6 +61,7 @@ import org.springframework.ai.ollama.api.OllamaApi.Message.ToolCall;
|
||||
import org.springframework.ai.ollama.api.OllamaApi.Message.ToolCallFunction;
|
||||
import org.springframework.ai.ollama.api.OllamaModel;
|
||||
import org.springframework.ai.ollama.api.OllamaOptions;
|
||||
import org.springframework.ai.ollama.api.common.OllamaApiConstants;
|
||||
import org.springframework.ai.ollama.management.ModelManagementOptions;
|
||||
import org.springframework.ai.ollama.management.OllamaModelManager;
|
||||
import org.springframework.ai.ollama.management.PullModelStrategy;
|
||||
@@ -224,7 +225,7 @@ public class OllamaChatModel implements ChatModel {
|
||||
|
||||
ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
|
||||
.prompt(prompt)
|
||||
.provider(OllamaApi.PROVIDER_NAME)
|
||||
.provider(OllamaApiConstants.PROVIDER_NAME)
|
||||
.build();
|
||||
|
||||
ChatResponse response = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION
|
||||
@@ -294,7 +295,7 @@ public class OllamaChatModel implements ChatModel {
|
||||
|
||||
final ChatModelObservationContext observationContext = ChatModelObservationContext.builder()
|
||||
.prompt(prompt)
|
||||
.provider(OllamaApi.PROVIDER_NAME)
|
||||
.provider(OllamaApiConstants.PROVIDER_NAME)
|
||||
.build();
|
||||
|
||||
Observation observation = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION.observation(
|
||||
@@ -343,8 +344,7 @@ public class OllamaChatModel implements ChatModel {
|
||||
return Flux.just(ChatResponse.builder().from(response)
|
||||
.generations(ToolExecutionResult.buildGenerations(toolExecutionResult))
|
||||
.build());
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// Send the tool execution result back to the model.
|
||||
return this.internalStream(new Prompt(toolExecutionResult.conversationHistory(), prompt.getOptions()),
|
||||
response);
|
||||
|
||||
@@ -43,6 +43,7 @@ import org.springframework.ai.ollama.api.OllamaApi;
|
||||
import org.springframework.ai.ollama.api.OllamaApi.EmbeddingsResponse;
|
||||
import org.springframework.ai.ollama.api.OllamaModel;
|
||||
import org.springframework.ai.ollama.api.OllamaOptions;
|
||||
import org.springframework.ai.ollama.api.common.OllamaApiConstants;
|
||||
import org.springframework.ai.ollama.management.ModelManagementOptions;
|
||||
import org.springframework.ai.ollama.management.OllamaModelManager;
|
||||
import org.springframework.ai.ollama.management.PullModelStrategy;
|
||||
@@ -112,7 +113,7 @@ public class OllamaEmbeddingModel extends AbstractEmbeddingModel {
|
||||
|
||||
var observationContext = EmbeddingModelObservationContext.builder()
|
||||
.embeddingRequest(request)
|
||||
.provider(OllamaApi.PROVIDER_NAME)
|
||||
.provider(OllamaApiConstants.PROVIDER_NAME)
|
||||
.build();
|
||||
|
||||
return EmbeddingModelObservationDocumentation.EMBEDDING_MODEL_OPERATION
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
* Copyright 2023-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -30,18 +30,17 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.ai.ollama.api.common.OllamaApiConstants;
|
||||
import org.springframework.ai.retry.RetryUtils;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.ai.model.ModelOptionsUtils;
|
||||
import org.springframework.ai.observation.conventions.AiProvider;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.web.client.ResponseErrorHandler;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
@@ -51,21 +50,18 @@ import org.springframework.web.reactive.function.client.WebClient;
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @author Thomas Vitale
|
||||
* @author Jonghoon Park
|
||||
* @since 0.8.0
|
||||
*/
|
||||
// @formatter:off
|
||||
public class OllamaApi {
|
||||
|
||||
public static final String PROVIDER_NAME = AiProvider.OLLAMA.value();
|
||||
public static Builder builder() { return new Builder(); }
|
||||
|
||||
public static final String REQUEST_BODY_NULL_ERROR = "The request body can not be null.";
|
||||
|
||||
private static final Log logger = LogFactory.getLog(OllamaApi.class);
|
||||
|
||||
private static final String DEFAULT_BASE_URL = "http://localhost:11434";
|
||||
|
||||
private final ResponseErrorHandler responseErrorHandler;
|
||||
|
||||
private final RestClient restClient;
|
||||
|
||||
private final WebClient webClient;
|
||||
@@ -73,16 +69,18 @@ public class OllamaApi {
|
||||
/**
|
||||
* Default constructor that uses the default localhost url.
|
||||
*/
|
||||
@Deprecated(since = "1.0.0.M8")
|
||||
public OllamaApi() {
|
||||
this(DEFAULT_BASE_URL);
|
||||
this(OllamaApiConstants.DEFAULT_BASE_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crate a new OllamaApi instance with the given base url.
|
||||
* @param baseUrl The base url of the Ollama server.
|
||||
*/
|
||||
@Deprecated(since = "1.0.0.M8")
|
||||
public OllamaApi(String baseUrl) {
|
||||
this(baseUrl, RestClient.builder(), WebClient.builder());
|
||||
this(baseUrl, RestClient.builder(), WebClient.builder(), RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,19 +88,36 @@ public class OllamaApi {
|
||||
* {@link RestClient.Builder}.
|
||||
* @param baseUrl The base url of the Ollama server.
|
||||
* @param restClientBuilder The {@link RestClient.Builder} to use.
|
||||
* @param webClientBuilder The {@link WebClient.Builder} to use.
|
||||
*/
|
||||
@Deprecated(since = "1.0.0.M8")
|
||||
public OllamaApi(String baseUrl, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder) {
|
||||
this(baseUrl, restClientBuilder, webClientBuilder, RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER);
|
||||
}
|
||||
|
||||
this.responseErrorHandler = new OllamaResponseErrorHandler();
|
||||
/**
|
||||
* Create a new OllamaApi instance
|
||||
* @param baseUrl The base url of the Ollama server.
|
||||
* @param restClientBuilder The {@link RestClient.Builder} to use.
|
||||
* @param webClientBuilder The {@link WebClient.Builder} to use.
|
||||
* @param responseErrorHandler Response error handler.
|
||||
*/
|
||||
private OllamaApi(String baseUrl, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) {
|
||||
|
||||
Consumer<HttpHeaders> defaultHeaders = headers -> {
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setAccept(List.of(MediaType.APPLICATION_JSON));
|
||||
};
|
||||
|
||||
this.restClient = restClientBuilder.baseUrl(baseUrl).defaultHeaders(defaultHeaders).build();
|
||||
this.restClient = restClientBuilder.baseUrl(baseUrl)
|
||||
.defaultHeaders(defaultHeaders)
|
||||
.defaultStatusHandler(responseErrorHandler)
|
||||
.build();
|
||||
|
||||
this.webClient = webClientBuilder.baseUrl(baseUrl).defaultHeaders(defaultHeaders).build();
|
||||
this.webClient = webClientBuilder
|
||||
.baseUrl(baseUrl)
|
||||
.defaultHeaders(defaultHeaders)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -121,7 +136,6 @@ public class OllamaApi {
|
||||
.uri("/api/chat")
|
||||
.body(chatRequest)
|
||||
.retrieve()
|
||||
.onStatus(this.responseErrorHandler)
|
||||
.body(ChatResponse.class);
|
||||
}
|
||||
|
||||
@@ -188,7 +202,6 @@ public class OllamaApi {
|
||||
.uri("/api/embed")
|
||||
.body(embeddingsRequest)
|
||||
.retrieve()
|
||||
.onStatus(this.responseErrorHandler)
|
||||
.body(EmbeddingsResponse.class);
|
||||
}
|
||||
|
||||
@@ -199,7 +212,6 @@ public class OllamaApi {
|
||||
return this.restClient.get()
|
||||
.uri("/api/tags")
|
||||
.retrieve()
|
||||
.onStatus(this.responseErrorHandler)
|
||||
.body(ListModelResponse.class);
|
||||
}
|
||||
|
||||
@@ -212,7 +224,6 @@ public class OllamaApi {
|
||||
.uri("/api/show")
|
||||
.body(showModelRequest)
|
||||
.retrieve()
|
||||
.onStatus(this.responseErrorHandler)
|
||||
.body(ShowModelResponse.class);
|
||||
}
|
||||
|
||||
@@ -225,7 +236,6 @@ public class OllamaApi {
|
||||
.uri("/api/copy")
|
||||
.body(copyModelRequest)
|
||||
.retrieve()
|
||||
.onStatus(this.responseErrorHandler)
|
||||
.toBodilessEntity();
|
||||
}
|
||||
|
||||
@@ -238,7 +248,6 @@ public class OllamaApi {
|
||||
.uri("/api/delete")
|
||||
.body(deleteModelRequest)
|
||||
.retrieve()
|
||||
.onStatus(this.responseErrorHandler)
|
||||
.toBodilessEntity();
|
||||
}
|
||||
|
||||
@@ -261,26 +270,6 @@ public class OllamaApi {
|
||||
.bodyToFlux(ProgressResponse.class);
|
||||
}
|
||||
|
||||
private static class OllamaResponseErrorHandler implements ResponseErrorHandler {
|
||||
|
||||
@Override
|
||||
public boolean hasError(ClientHttpResponse response) throws IOException {
|
||||
return response.getStatusCode().isError();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleError(ClientHttpResponse response) throws IOException {
|
||||
if (response.getStatusCode().isError()) {
|
||||
int statusCode = response.getStatusCode().value();
|
||||
String statusText = response.getStatusText();
|
||||
String message = StreamUtils.copyToString(response.getBody(), java.nio.charset.StandardCharsets.UTF_8);
|
||||
logger.warn(String.format("[%s] %s - %s", statusCode, statusText, message));
|
||||
throw new RuntimeException(String.format("[%s] %s - %s", statusCode, statusText, message));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Chat message object.
|
||||
*
|
||||
@@ -736,5 +725,44 @@ public class OllamaApi {
|
||||
@JsonProperty("completed") Long completed
|
||||
) { }
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private String baseUrl = OllamaApiConstants.DEFAULT_BASE_URL;
|
||||
|
||||
private RestClient.Builder restClientBuilder = RestClient.builder();
|
||||
|
||||
private WebClient.Builder webClientBuilder = WebClient.builder();
|
||||
|
||||
private ResponseErrorHandler responseErrorHandler = RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER;
|
||||
|
||||
public Builder baseUrl(String baseUrl) {
|
||||
Assert.hasText(baseUrl, "baseUrl cannot be null or empty");
|
||||
this.baseUrl = baseUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder restClientBuilder(RestClient.Builder restClientBuilder) {
|
||||
Assert.notNull(restClientBuilder, "restClientBuilder cannot be null");
|
||||
this.restClientBuilder = restClientBuilder;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder webClientBuilder(WebClient.Builder webClientBuilder) {
|
||||
Assert.notNull(webClientBuilder, "webClientBuilder cannot be null");
|
||||
this.webClientBuilder = webClientBuilder;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder responseErrorHandler(ResponseErrorHandler responseErrorHandler) {
|
||||
Assert.notNull(responseErrorHandler, "responseErrorHandler cannot be null");
|
||||
this.responseErrorHandler = responseErrorHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
public OllamaApi build() {
|
||||
return new OllamaApi(this.baseUrl, this.restClientBuilder, this.webClientBuilder, this.responseErrorHandler);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2023-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.ai.ollama.api.common;
|
||||
|
||||
import org.springframework.ai.observation.conventions.AiProvider;
|
||||
|
||||
/**
|
||||
* Common value constants for Ollama api.
|
||||
*
|
||||
* @author Jonghoon Park
|
||||
*/
|
||||
public final class OllamaApiConstants {
|
||||
|
||||
public static final String DEFAULT_BASE_URL = "http://localhost:11434";
|
||||
|
||||
public static final String PROVIDER_NAME = AiProvider.OLLAMA.value();
|
||||
|
||||
private OllamaApiConstants() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
* Copyright 2023-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -86,7 +86,7 @@ public abstract class BaseOllamaIT {
|
||||
|
||||
private static OllamaApi buildOllamaApiWithModel(final String model) {
|
||||
final String baseUrl = SKIP_CONTAINER_CREATION ? OLLAMA_LOCAL_URL : ollamaContainer.getEndpoint();
|
||||
final OllamaApi api = new OllamaApi(baseUrl);
|
||||
final OllamaApi api = OllamaApi.builder().baseUrl(baseUrl).build();
|
||||
ensureModelIsPresent(api, model);
|
||||
return api;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
class OllamaChatRequestTests {
|
||||
|
||||
OllamaChatModel chatModel = OllamaChatModel.builder()
|
||||
.ollamaApi(new OllamaApi())
|
||||
.ollamaApi(OllamaApi.builder().build())
|
||||
.defaultOptions(OllamaOptions.builder().model("MODEL_NAME").topK(99).temperature(66.6).numGPU(1).build())
|
||||
.build();
|
||||
|
||||
@@ -51,7 +51,7 @@ class OllamaChatRequestTests {
|
||||
.toolContext(Map.of("key1", "value1", "key2", "valueA"))
|
||||
.build();
|
||||
OllamaChatModel chatModel = OllamaChatModel.builder()
|
||||
.ollamaApi(new OllamaApi())
|
||||
.ollamaApi(OllamaApi.builder().build())
|
||||
.defaultOptions(defaultOptions)
|
||||
.build();
|
||||
|
||||
@@ -143,7 +143,7 @@ class OllamaChatRequestTests {
|
||||
@Test
|
||||
public void createRequestWithDefaultOptionsModelOverride() {
|
||||
OllamaChatModel chatModel = OllamaChatModel.builder()
|
||||
.ollamaApi(new OllamaApi())
|
||||
.ollamaApi(OllamaApi.builder().build())
|
||||
.defaultOptions(OllamaOptions.builder().model("DEFAULT_OPTIONS_MODEL").build())
|
||||
.build();
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
public class OllamaEmbeddingRequestTests {
|
||||
|
||||
OllamaEmbeddingModel embeddingModel = OllamaEmbeddingModel.builder()
|
||||
.ollamaApi(new OllamaApi())
|
||||
.ollamaApi(OllamaApi.builder().build())
|
||||
.defaultOptions(OllamaOptions.builder().model("DEFAULT_MODEL").mainGPU(11).useMMap(true).numGPU(1).build())
|
||||
.build();
|
||||
|
||||
|
||||
@@ -483,7 +483,7 @@ Next, create an `OllamaChatModel` instance and use it to send requests for text
|
||||
|
||||
[source,java]
|
||||
----
|
||||
var ollamaApi = new OllamaApi();
|
||||
var ollamaApi = OllamaApi.builder().build();
|
||||
|
||||
var chatModel = OllamaChatModel.builder()
|
||||
.ollamaApi(ollamaApi)
|
||||
|
||||
@@ -319,7 +319,7 @@ Next, create an `OllamaEmbeddingModel` instance and use it to compute the embedd
|
||||
|
||||
[source,java]
|
||||
----
|
||||
var ollamaApi = new OllamaApi();
|
||||
var ollamaApi = OllamaApi.builder().build();
|
||||
|
||||
var embeddingModel = new OllamaEmbeddingModel(this.ollamaApi,
|
||||
OllamaOptions.builder()
|
||||
|
||||
@@ -270,7 +270,7 @@ WARNING: Default tools are shared across all the chat requests performed by that
|
||||
----
|
||||
ToolCallback[] dateTimeTools = ToolCallbacks.from(new DateTimeTools());
|
||||
ChatModel chatModel = OllamaChatModel.builder()
|
||||
.ollamaApi(new OllamaApi())
|
||||
.ollamaApi(OllamaApi.builder().build())
|
||||
.defaultOptions(ToolCallingChatOptions.builder()
|
||||
.toolCallbacks(dateTimeTools)
|
||||
.build())
|
||||
@@ -438,7 +438,7 @@ WARNING: Default tools are shared across all the chat requests performed by that
|
||||
----
|
||||
ToolCallback toolCallback = ...
|
||||
ChatModel chatModel = OllamaChatModel.builder()
|
||||
.ollamaApi(new OllamaApi())
|
||||
.ollamaApi(OllamaApi.builder().build())
|
||||
.defaultOptions(ToolCallingChatOptions.builder()
|
||||
.toolCallbacks(toolCallback)
|
||||
.build())
|
||||
@@ -560,7 +560,7 @@ WARNING: Default tools are shared across all the chat requests performed by that
|
||||
----
|
||||
ToolCallback toolCallback = ...
|
||||
ChatModel chatModel = OllamaChatModel.builder()
|
||||
.ollamaApi(new OllamaApi())
|
||||
.ollamaApi(OllamaApi.builder().build())
|
||||
.defaultOptions(ToolCallingChatOptions.builder()
|
||||
.toolCallbacks(toolCallback)
|
||||
.build())
|
||||
@@ -667,7 +667,7 @@ WARNING: Default tools are shared across all the chat requests performed by that
|
||||
[source,java]
|
||||
----
|
||||
ChatModel chatModel = OllamaChatModel.builder()
|
||||
.ollamaApi(new OllamaApi())
|
||||
.ollamaApi(OllamaApi.builder().build())
|
||||
.defaultOptions(ToolCallingChatOptions.builder()
|
||||
.toolNames("currentWeather")
|
||||
.build())
|
||||
|
||||
Reference in New Issue
Block a user