Improve Ollama test container management
This change simplifies how we manage Ollama containers in tests by moving from manual toggles to environment variables for better control. Instead of scattered container configuration, we now have: - OLLAMA_WITH_REUSE: Toggle reuse of existing containers between tests - OLLAMA_TESTS_ENABLED: Control test execution globally The motivation is to make tests more reliable and easier to maintain. Previously, developers had to modify code to run tests locally vs CI. Now they can control this via environment variables. We also introduce thread-safe API access and consistent default settings across all test classes, removing duplicated configuration and potential resource leaks. This makes the test infrastructure more maintainable and provides clearer separation between local development and CI environments. Checkstyle fixes. Make buildOllamaApiWithModel in BaseOllamaIT public and the related test changes.
This commit is contained in:
committed by
Soby Chacko
parent
c3c95a82c1
commit
21fc653464
@@ -18,66 +18,87 @@ package org.springframework.ai.ollama;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import org.testcontainers.ollama.OllamaContainer;
|
||||
|
||||
import org.springframework.ai.ollama.api.OllamaApi;
|
||||
import org.springframework.ai.ollama.management.ModelManagementOptions;
|
||||
import org.springframework.ai.ollama.management.OllamaModelManager;
|
||||
import org.springframework.ai.ollama.management.PullModelStrategy;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
public class BaseOllamaIT {
|
||||
@Testcontainers
|
||||
@EnabledIfEnvironmentVariable(named = "OLLAMA_TESTS_ENABLED", matches = "true")
|
||||
public abstract class BaseOllamaIT {
|
||||
|
||||
// Toggle for running tests locally on native Ollama for a faster feedback loop.
|
||||
private static final boolean useTestcontainers = true;
|
||||
private static final String OLLAMA_LOCAL_URL = "http://localhost:11434";
|
||||
|
||||
public static OllamaContainer ollamaContainer;
|
||||
private static final Duration DEFAULT_TIMEOUT = Duration.ofMinutes(10);
|
||||
|
||||
static {
|
||||
if (useTestcontainers) {
|
||||
private static final int DEFAULT_MAX_RETRIES = 2;
|
||||
|
||||
// Environment variable to control whether to create a new container or use existing
|
||||
// Ollama instance
|
||||
private static final boolean SKIP_CONTAINER_CREATION = Boolean
|
||||
.parseBoolean(System.getenv().getOrDefault("OLLAMA_SKIP_CONTAINER", "false"));
|
||||
|
||||
private static OllamaContainer ollamaContainer;
|
||||
|
||||
private static final ThreadLocal<OllamaApi> ollamaApi = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* Initialize the Ollama container and API with the specified model. This method
|
||||
* should be called from @BeforeAll in subclasses.
|
||||
* @param model the Ollama model to initialize (must not be null or empty)
|
||||
* @return configured OllamaApi instance
|
||||
* @throws IllegalArgumentException if model is null or empty
|
||||
*/
|
||||
protected static OllamaApi initializeOllama(final String model) {
|
||||
Assert.hasText(model, "Model name must be provided");
|
||||
|
||||
if (!SKIP_CONTAINER_CREATION) {
|
||||
ollamaContainer = new OllamaContainer(OllamaImage.DEFAULT_IMAGE).withReuse(true);
|
||||
ollamaContainer.start();
|
||||
}
|
||||
|
||||
final OllamaApi api = buildOllamaApiWithModel(model);
|
||||
ollamaApi.set(api);
|
||||
return api;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the return value to false in order to run multiple Ollama IT tests locally
|
||||
* reusing the same container image.
|
||||
*
|
||||
* Also, add the entry
|
||||
*
|
||||
* testcontainers.reuse.enable=true
|
||||
*
|
||||
* to the file ".testcontainers.properties" located in your home directory
|
||||
* Get the initialized OllamaApi instance.
|
||||
* @return the OllamaApi instance
|
||||
* @throws IllegalStateException if called before initialization
|
||||
*/
|
||||
public static boolean isDisabled() {
|
||||
return true;
|
||||
protected static OllamaApi getOllamaApi() {
|
||||
OllamaApi api = ollamaApi.get();
|
||||
Assert.state(api != null, "OllamaApi not initialized. Call initializeOllama first.");
|
||||
return api;
|
||||
}
|
||||
|
||||
public static OllamaApi buildOllamaApi() {
|
||||
return buildOllamaApiWithModel(null);
|
||||
}
|
||||
|
||||
public static OllamaApi buildOllamaApiWithModel(String model) {
|
||||
var baseUrl = "http://localhost:11434";
|
||||
if (useTestcontainers) {
|
||||
baseUrl = ollamaContainer.getEndpoint();
|
||||
@AfterAll
|
||||
public static void tearDown() {
|
||||
if (ollamaContainer != null) {
|
||||
ollamaContainer.stop();
|
||||
}
|
||||
var ollamaApi = new OllamaApi(baseUrl);
|
||||
|
||||
if (StringUtils.hasText(model)) {
|
||||
ensureModelIsPresent(ollamaApi, model);
|
||||
}
|
||||
|
||||
return ollamaApi;
|
||||
}
|
||||
|
||||
public static void ensureModelIsPresent(OllamaApi ollamaApi, String model) {
|
||||
var modelManagementOptions = ModelManagementOptions.builder()
|
||||
.withMaxRetries(2)
|
||||
.withTimeout(Duration.ofMinutes(10))
|
||||
private static OllamaApi buildOllamaApiWithModel(final String model) {
|
||||
final String baseUrl = SKIP_CONTAINER_CREATION ? OLLAMA_LOCAL_URL : ollamaContainer.getEndpoint();
|
||||
final OllamaApi api = new OllamaApi(baseUrl);
|
||||
ensureModelIsPresent(api, model);
|
||||
return api;
|
||||
}
|
||||
|
||||
private static void ensureModelIsPresent(final OllamaApi ollamaApi, final String model) {
|
||||
final var modelManagementOptions = ModelManagementOptions.builder()
|
||||
.withMaxRetries(DEFAULT_MAX_RETRIES)
|
||||
.withTimeout(DEFAULT_TIMEOUT)
|
||||
.build();
|
||||
var ollamaModelManager = new OllamaModelManager(ollamaApi, modelManagementOptions);
|
||||
final var ollamaModelManager = new OllamaModelManager(ollamaApi, modelManagementOptions);
|
||||
ollamaModelManager.pullModel(model, PullModelStrategy.WHEN_MISSING);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,10 +22,8 @@ import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledIf;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.ai.chat.messages.AssistantMessage;
|
||||
@@ -46,9 +44,7 @@ import org.springframework.context.annotation.Bean;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@Testcontainers
|
||||
@SpringBootTest(classes = OllamaChatModelFunctionCallingIT.Config.class)
|
||||
@DisabledIf("isDisabled")
|
||||
class OllamaChatModelFunctionCallingIT extends BaseOllamaIT {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(OllamaChatModelFunctionCallingIT.class);
|
||||
@@ -120,7 +116,7 @@ class OllamaChatModelFunctionCallingIT extends BaseOllamaIT {
|
||||
|
||||
@Bean
|
||||
public OllamaApi ollamaApi() {
|
||||
return buildOllamaApiWithModel(MODEL);
|
||||
return initializeOllama(MODEL);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -21,8 +21,6 @@ import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledIf;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.messages.AssistantMessage;
|
||||
@@ -53,8 +51,6 @@ import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@SpringBootTest
|
||||
@Testcontainers
|
||||
@DisabledIf("isDisabled")
|
||||
class OllamaChatModelIT extends BaseOllamaIT {
|
||||
|
||||
private static final String MODEL = OllamaModel.LLAMA3_2.getName();
|
||||
@@ -241,7 +237,7 @@ class OllamaChatModelIT extends BaseOllamaIT {
|
||||
|
||||
@Bean
|
||||
public OllamaApi ollamaApi() {
|
||||
return buildOllamaApiWithModel(MODEL);
|
||||
return initializeOllama(MODEL);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -19,10 +19,8 @@ package org.springframework.ai.ollama;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledIf;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
@@ -40,8 +38,6 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
|
||||
|
||||
@SpringBootTest
|
||||
@Testcontainers
|
||||
@DisabledIf("isDisabled")
|
||||
class OllamaChatModelMultimodalIT extends BaseOllamaIT {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(OllamaChatModelMultimodalIT.class);
|
||||
@@ -80,7 +76,7 @@ class OllamaChatModelMultimodalIT extends BaseOllamaIT {
|
||||
|
||||
@Bean
|
||||
public OllamaApi ollamaApi() {
|
||||
return buildOllamaApiWithModel(MODEL);
|
||||
return initializeOllama(MODEL);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -23,7 +23,6 @@ import io.micrometer.observation.tck.TestObservationRegistry;
|
||||
import io.micrometer.observation.tck.TestObservationRegistryAssert;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledIf;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.ai.chat.metadata.ChatResponseMetadata;
|
||||
@@ -50,7 +49,6 @@ import static org.springframework.ai.chat.observation.ChatModelObservationDocume
|
||||
* @author Thomas Vitale
|
||||
*/
|
||||
@SpringBootTest(classes = OllamaChatModelObservationIT.Config.class)
|
||||
@DisabledIf("isDisabled")
|
||||
public class OllamaChatModelObservationIT extends BaseOllamaIT {
|
||||
|
||||
private static final String MODEL = OllamaModel.LLAMA3_2.getName();
|
||||
@@ -166,7 +164,7 @@ public class OllamaChatModelObservationIT extends BaseOllamaIT {
|
||||
|
||||
@Bean
|
||||
public OllamaApi openAiApi() {
|
||||
return buildOllamaApiWithModel(MODEL);
|
||||
return initializeOllama(MODEL);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -19,8 +19,6 @@ package org.springframework.ai.ollama;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledIf;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
|
||||
import org.springframework.ai.embedding.EmbeddingRequest;
|
||||
import org.springframework.ai.embedding.EmbeddingResponse;
|
||||
@@ -38,8 +36,6 @@ import org.springframework.context.annotation.Bean;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@SpringBootTest
|
||||
@DisabledIf("isDisabled")
|
||||
@Testcontainers
|
||||
class OllamaEmbeddingModelIT extends BaseOllamaIT {
|
||||
|
||||
private static final String MODEL = OllamaModel.NOMIC_EMBED_TEXT.getName();
|
||||
@@ -100,7 +96,7 @@ class OllamaEmbeddingModelIT extends BaseOllamaIT {
|
||||
|
||||
@Bean
|
||||
public OllamaApi ollamaApi() {
|
||||
return buildOllamaApiWithModel(MODEL);
|
||||
return initializeOllama(MODEL);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -21,7 +21,6 @@ import java.util.List;
|
||||
import io.micrometer.observation.tck.TestObservationRegistry;
|
||||
import io.micrometer.observation.tck.TestObservationRegistryAssert;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledIf;
|
||||
|
||||
import org.springframework.ai.embedding.EmbeddingRequest;
|
||||
import org.springframework.ai.embedding.EmbeddingResponse;
|
||||
@@ -47,7 +46,6 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
* @author Thomas Vitale
|
||||
*/
|
||||
@SpringBootTest(classes = OllamaEmbeddingModelObservationIT.Config.class)
|
||||
@DisabledIf("isDisabled")
|
||||
public class OllamaEmbeddingModelObservationIT extends BaseOllamaIT {
|
||||
|
||||
private static final String MODEL = OllamaModel.NOMIC_EMBED_TEXT.getName();
|
||||
@@ -100,7 +98,7 @@ public class OllamaEmbeddingModelObservationIT extends BaseOllamaIT {
|
||||
|
||||
@Bean
|
||||
public OllamaApi openAiApi() {
|
||||
return buildOllamaApiWithModel(MODEL);
|
||||
return initializeOllama(MODEL);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -22,8 +22,6 @@ import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledIf;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.ai.ollama.BaseOllamaIT;
|
||||
@@ -42,17 +40,13 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
* @author Christian Tzolov
|
||||
* @author Thomas Vitale
|
||||
*/
|
||||
@Testcontainers
|
||||
@DisabledIf("isDisabled")
|
||||
public class OllamaApiIT extends BaseOllamaIT {
|
||||
|
||||
private static final String MODEL = OllamaModel.LLAMA3_2.getName();
|
||||
|
||||
static OllamaApi ollamaApi;
|
||||
|
||||
@BeforeAll
|
||||
public static void beforeAll() throws IOException, InterruptedException {
|
||||
ollamaApi = buildOllamaApiWithModel(MODEL);
|
||||
initializeOllama(MODEL);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -63,7 +57,7 @@ public class OllamaApiIT extends BaseOllamaIT {
|
||||
.withStream(false)
|
||||
.build();
|
||||
|
||||
GenerateResponse response = ollamaApi.generate(request);
|
||||
GenerateResponse response = getOllamaApi().generate(request);
|
||||
|
||||
System.out.println(response);
|
||||
|
||||
@@ -87,7 +81,7 @@ public class OllamaApiIT extends BaseOllamaIT {
|
||||
.withOptions(OllamaOptions.create().withTemperature(0.9))
|
||||
.build();
|
||||
|
||||
ChatResponse response = ollamaApi.chat(request);
|
||||
ChatResponse response = getOllamaApi().chat(request);
|
||||
|
||||
System.out.println(response);
|
||||
|
||||
@@ -108,7 +102,7 @@ public class OllamaApiIT extends BaseOllamaIT {
|
||||
.withOptions(OllamaOptions.create().withTemperature(0.9).toMap())
|
||||
.build();
|
||||
|
||||
Flux<ChatResponse> response = ollamaApi.streamingChat(request);
|
||||
Flux<ChatResponse> response = getOllamaApi().streamingChat(request);
|
||||
|
||||
List<ChatResponse> responses = response.collectList().block();
|
||||
System.out.println(responses);
|
||||
@@ -128,7 +122,7 @@ public class OllamaApiIT extends BaseOllamaIT {
|
||||
public void embedText() {
|
||||
EmbeddingsRequest request = new EmbeddingsRequest(MODEL, "I like to eat apples");
|
||||
|
||||
EmbeddingsResponse response = ollamaApi.embed(request);
|
||||
EmbeddingsResponse response = getOllamaApi().embed(request);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.embeddings()).hasSize(1);
|
||||
|
||||
@@ -21,8 +21,6 @@ import java.time.Duration;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledIf;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
|
||||
import org.springframework.ai.ollama.BaseOllamaIT;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -34,8 +32,6 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
*
|
||||
* @author Thomas Vitale
|
||||
*/
|
||||
@Testcontainers
|
||||
@DisabledIf("isDisabled")
|
||||
public class OllamaApiModelsIT extends BaseOllamaIT {
|
||||
|
||||
private static final String MODEL = "all-minilm";
|
||||
@@ -44,7 +40,7 @@ public class OllamaApiModelsIT extends BaseOllamaIT {
|
||||
|
||||
@BeforeAll
|
||||
public static void beforeAll() throws IOException, InterruptedException {
|
||||
ollamaApi = buildOllamaApiWithModel(MODEL);
|
||||
ollamaApi = initializeOllama(MODEL);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -23,10 +23,8 @@ import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledIf;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
|
||||
import org.springframework.ai.model.ModelOptionsUtils;
|
||||
import org.springframework.ai.ollama.BaseOllamaIT;
|
||||
@@ -42,8 +40,6 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
* @author Christian Tzolov
|
||||
* @author Thomas Vitale
|
||||
*/
|
||||
@Testcontainers
|
||||
@DisabledIf("isDisabled")
|
||||
public class OllamaApiToolFunctionCallIT extends BaseOllamaIT {
|
||||
|
||||
private static final String MODEL = "qwen2.5:3b";
|
||||
@@ -56,7 +52,7 @@ public class OllamaApiToolFunctionCallIT extends BaseOllamaIT {
|
||||
|
||||
@BeforeAll
|
||||
public static void beforeAll() throws IOException, InterruptedException {
|
||||
ollamaApi = buildOllamaApiWithModel(MODEL);
|
||||
ollamaApi = initializeOllama(MODEL);
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
|
||||
@@ -22,8 +22,6 @@ import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledIf;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
|
||||
import org.springframework.ai.ollama.BaseOllamaIT;
|
||||
import org.springframework.ai.ollama.api.OllamaModel;
|
||||
@@ -35,8 +33,6 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
*
|
||||
* @author Thomas Vitale
|
||||
*/
|
||||
@Testcontainers
|
||||
@DisabledIf("isDisabled")
|
||||
class OllamaModelManagerIT extends BaseOllamaIT {
|
||||
|
||||
private static final String MODEL = OllamaModel.NOMIC_EMBED_TEXT.getName();
|
||||
@@ -45,7 +41,7 @@ class OllamaModelManagerIT extends BaseOllamaIT {
|
||||
|
||||
@BeforeAll
|
||||
public static void beforeAll() throws IOException, InterruptedException {
|
||||
var ollamaApi = buildOllamaApiWithModel(MODEL);
|
||||
var ollamaApi = initializeOllama(MODEL);
|
||||
modelManager = new OllamaModelManager(ollamaApi);
|
||||
}
|
||||
|
||||
@@ -144,7 +140,7 @@ class OllamaModelManagerIT extends BaseOllamaIT {
|
||||
var isModelAvailable = modelManager.isModelAvailable(model);
|
||||
assertThat(isModelAvailable).isFalse();
|
||||
|
||||
new OllamaModelManager(buildOllamaApi(),
|
||||
new OllamaModelManager(getOllamaApi(),
|
||||
new ModelManagementOptions(PullModelStrategy.WHEN_MISSING, List.of(model), Duration.ofMinutes(5), 0));
|
||||
|
||||
isModelAvailable = modelManager.isModelAvailable(model);
|
||||
|
||||
@@ -19,27 +19,65 @@ package org.springframework.ai.autoconfigure.ollama;
|
||||
import java.time.Duration;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import org.testcontainers.ollama.OllamaContainer;
|
||||
|
||||
import org.springframework.ai.ollama.api.OllamaApi;
|
||||
import org.springframework.ai.ollama.management.ModelManagementOptions;
|
||||
import org.springframework.ai.ollama.management.OllamaModelManager;
|
||||
import org.springframework.ai.ollama.management.PullModelStrategy;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
public class BaseOllamaIT {
|
||||
@Testcontainers
|
||||
@EnabledIfEnvironmentVariable(named = "OLLAMA_TESTS_ENABLED", matches = "true")
|
||||
public abstract class BaseOllamaIT {
|
||||
|
||||
private static final String OLLAMA_LOCAL_URL = "http://localhost:11434";
|
||||
|
||||
private static final Duration DEFAULT_TIMEOUT = Duration.ofMinutes(10);
|
||||
|
||||
private static final int DEFAULT_MAX_RETRIES = 2;
|
||||
|
||||
// Environment variable to control whether to create a new container or use existing
|
||||
// Ollama instance
|
||||
private static final boolean SKIP_CONTAINER_CREATION = Boolean
|
||||
.parseBoolean(System.getenv().getOrDefault("OLLAMA_WITH_REUSE", "false"));
|
||||
|
||||
private static OllamaContainer ollamaContainer;
|
||||
|
||||
// Toggle for running tests locally on native Ollama for a faster feedback loop.
|
||||
private static final boolean useTestcontainers = true;
|
||||
private static final ThreadLocal<OllamaApi> ollamaApi = new ThreadLocal<>();
|
||||
|
||||
@BeforeAll
|
||||
public static void setUp() {
|
||||
if (useTestcontainers && !isDisabled()) {
|
||||
ollamaContainer = new OllamaContainer(OllamaImage.IMAGE).withReuse(true);
|
||||
/**
|
||||
* Initialize the Ollama API with the specified model. When OLLAMA_WITH_REUSE=true
|
||||
* (default), uses TestContainers withReuse feature. When OLLAMA_WITH_REUSE=false,
|
||||
* connects to local Ollama instance.
|
||||
* @param model the Ollama model to initialize (must not be null or empty)
|
||||
* @return configured OllamaApi instance
|
||||
* @throws IllegalArgumentException if model is null or empty
|
||||
*/
|
||||
protected static OllamaApi initializeOllama(final String model) {
|
||||
Assert.hasText(model, "Model name must be provided");
|
||||
|
||||
if (!SKIP_CONTAINER_CREATION) {
|
||||
ollamaContainer = new OllamaContainer(OllamaImage.DEFAULT_IMAGE).withReuse(true);
|
||||
ollamaContainer.start();
|
||||
}
|
||||
|
||||
final OllamaApi api = buildOllamaApiWithModel(model);
|
||||
ollamaApi.set(api);
|
||||
return api;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the initialized OllamaApi instance.
|
||||
* @return the OllamaApi instance
|
||||
* @throws IllegalStateException if called before initialization
|
||||
*/
|
||||
protected static OllamaApi getOllamaApi() {
|
||||
OllamaApi api = ollamaApi.get();
|
||||
Assert.state(api != null, "OllamaApi not initialized. Call initializeOllama first.");
|
||||
return api;
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
@@ -49,33 +87,25 @@ public class BaseOllamaIT {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the return value to false in order to run multiple Ollama IT tests locally
|
||||
* reusing the same container image.
|
||||
*
|
||||
* Also, add the entry
|
||||
*
|
||||
* testcontainers.reuse.enable=true
|
||||
*
|
||||
* to the file ".testcontainers.properties" located in your home directory
|
||||
*/
|
||||
public static boolean isDisabled() {
|
||||
return true;
|
||||
public static OllamaApi buildOllamaApiWithModel(final String model) {
|
||||
final String baseUrl = SKIP_CONTAINER_CREATION ? OLLAMA_LOCAL_URL : ollamaContainer.getEndpoint();
|
||||
final OllamaApi api = new OllamaApi(baseUrl);
|
||||
ensureModelIsPresent(api, model);
|
||||
return api;
|
||||
}
|
||||
|
||||
public static String buildConnectionWithModel(String model) {
|
||||
var baseUrl = "http://localhost:11434";
|
||||
if (useTestcontainers) {
|
||||
baseUrl = ollamaContainer.getEndpoint();
|
||||
}
|
||||
|
||||
var modelManagementOptions = ModelManagementOptions.builder()
|
||||
.withMaxRetries(2)
|
||||
.withTimeout(Duration.ofMinutes(10))
|
||||
.build();
|
||||
var ollamaModelManager = new OllamaModelManager(new OllamaApi(baseUrl), modelManagementOptions);
|
||||
ollamaModelManager.pullModel(model, PullModelStrategy.WHEN_MISSING);
|
||||
public String getBaseUrl() {
|
||||
String baseUrl = SKIP_CONTAINER_CREATION ? OLLAMA_LOCAL_URL : ollamaContainer.getEndpoint();
|
||||
return baseUrl;
|
||||
}
|
||||
|
||||
private static void ensureModelIsPresent(final OllamaApi ollamaApi, final String model) {
|
||||
final var modelManagementOptions = ModelManagementOptions.builder()
|
||||
.withMaxRetries(DEFAULT_MAX_RETRIES)
|
||||
.withTimeout(DEFAULT_TIMEOUT)
|
||||
.build();
|
||||
final var ollamaModelManager = new OllamaModelManager(ollamaApi, modelManagementOptions);
|
||||
ollamaModelManager.pullModel(model, PullModelStrategy.WHEN_MISSING);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,8 +22,6 @@ import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledIf;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.ai.chat.messages.AssistantMessage;
|
||||
@@ -46,17 +44,13 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
* @author Thomas Vitale
|
||||
* @since 0.8.0
|
||||
*/
|
||||
@Testcontainers
|
||||
@DisabledIf("isDisabled")
|
||||
public class OllamaChatAutoConfigurationIT extends BaseOllamaIT {
|
||||
|
||||
private static final String MODEL_NAME = OllamaModel.LLAMA3_2.getName();
|
||||
|
||||
static String baseUrl;
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withPropertyValues(
|
||||
// @formatter:off
|
||||
"spring.ai.ollama.baseUrl=" + baseUrl,
|
||||
"spring.ai.ollama.baseUrl=" + getBaseUrl(),
|
||||
"spring.ai.ollama.chat.options.model=" + MODEL_NAME,
|
||||
"spring.ai.ollama.chat.options.temperature=0.5",
|
||||
"spring.ai.ollama.chat.options.topK=10")
|
||||
@@ -67,7 +61,7 @@ public class OllamaChatAutoConfigurationIT extends BaseOllamaIT {
|
||||
|
||||
@BeforeAll
|
||||
public static void beforeAll() throws IOException, InterruptedException {
|
||||
baseUrl = buildConnectionWithModel(MODEL_NAME);
|
||||
initializeOllama(MODEL_NAME);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -21,8 +21,6 @@ import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledIf;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
|
||||
import org.springframework.ai.embedding.EmbeddingResponse;
|
||||
import org.springframework.ai.ollama.OllamaEmbeddingModel;
|
||||
@@ -38,24 +36,20 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
* @author Thomas Vitale
|
||||
* @since 0.8.0
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Testcontainers
|
||||
@DisabledIf("isDisabled")
|
||||
public class OllamaEmbeddingAutoConfigurationIT extends BaseOllamaIT {
|
||||
|
||||
private static final String MODEL_NAME = OllamaModel.NOMIC_EMBED_TEXT.getName();
|
||||
|
||||
static String baseUrl;
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withPropertyValues("spring.ai.ollama.embedding.options.model=" + MODEL_NAME,
|
||||
"spring.ai.ollama.base-url=" + baseUrl)
|
||||
"spring.ai.ollama.base-url=" + getBaseUrl())
|
||||
.withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class, OllamaAutoConfiguration.class));
|
||||
|
||||
@BeforeAll
|
||||
public static void beforeAll() throws IOException, InterruptedException {
|
||||
baseUrl = buildConnectionWithModel(MODEL_NAME);
|
||||
initializeOllama(MODEL_NAME);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -18,7 +18,7 @@ package org.springframework.ai.autoconfigure.ollama;
|
||||
|
||||
public final class OllamaImage {
|
||||
|
||||
public static final String IMAGE = "ollama/ollama:0.3.14";
|
||||
public static final String DEFAULT_IMAGE = "ollama/ollama:0.3.14";
|
||||
|
||||
private OllamaImage() {
|
||||
|
||||
|
||||
@@ -22,10 +22,8 @@ import java.util.stream.Collectors;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledIf;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.ai.autoconfigure.ollama.BaseOllamaIT;
|
||||
@@ -43,19 +41,15 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@Testcontainers
|
||||
@DisabledIf("isDisabled")
|
||||
public class FunctionCallbackInPromptIT extends BaseOllamaIT {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(FunctionCallbackInPromptIT.class);
|
||||
|
||||
private static final String MODEL_NAME = "qwen2.5:3b";
|
||||
|
||||
static String baseUrl;
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withPropertyValues(
|
||||
// @formatter:off
|
||||
"spring.ai.ollama.baseUrl=" + baseUrl,
|
||||
"spring.ai.ollama.baseUrl=" + getBaseUrl(),
|
||||
"spring.ai.ollama.chat.options.model=" + MODEL_NAME,
|
||||
"spring.ai.ollama.chat.options.temperature=0.5",
|
||||
"spring.ai.ollama.chat.options.topK=10")
|
||||
@@ -64,7 +58,7 @@ public class FunctionCallbackInPromptIT extends BaseOllamaIT {
|
||||
|
||||
@BeforeAll
|
||||
public static void beforeAll() {
|
||||
baseUrl = buildConnectionWithModel(MODEL_NAME);
|
||||
initializeOllama(MODEL_NAME);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -22,10 +22,8 @@ import java.util.stream.Collectors;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledIf;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.ai.autoconfigure.ollama.BaseOllamaIT;
|
||||
@@ -48,19 +46,15 @@ import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@Testcontainers
|
||||
@DisabledIf("isDisabled")
|
||||
public class FunctionCallbackWrapperIT extends BaseOllamaIT {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(FunctionCallbackWrapperIT.class);
|
||||
|
||||
private static final String MODEL_NAME = "qwen2.5:3b";
|
||||
|
||||
static String baseUrl;
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withPropertyValues(
|
||||
// @formatter:off
|
||||
"spring.ai.ollama.baseUrl=" + baseUrl,
|
||||
"spring.ai.ollama.baseUrl=" + getBaseUrl(),
|
||||
"spring.ai.ollama.chat.options.model=" + MODEL_NAME,
|
||||
"spring.ai.ollama.chat.options.temperature=0.5",
|
||||
"spring.ai.ollama.chat.options.topK=10")
|
||||
@@ -70,7 +64,7 @@ public class FunctionCallbackWrapperIT extends BaseOllamaIT {
|
||||
|
||||
@BeforeAll
|
||||
public static void beforeAll() {
|
||||
baseUrl = buildConnectionWithModel(MODEL_NAME);
|
||||
initializeOllama(MODEL_NAME);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -43,7 +43,7 @@ class FunctionCallbackContextKotlinIT : BaseOllamaIT() {
|
||||
|
||||
private val MODEL_NAME = "qwen2.5:3b"
|
||||
|
||||
val contextRunner = buildConnectionWithModel(MODEL_NAME).let { baseUrl ->
|
||||
val contextRunner = buildOllamaApiWithModel(MODEL_NAME).let { baseUrl ->
|
||||
ApplicationContextRunner().withPropertyValues(
|
||||
"spring.ai.ollama.baseUrl=$baseUrl",
|
||||
"spring.ai.ollama.chat.options.model=$MODEL_NAME",
|
||||
|
||||
@@ -44,7 +44,7 @@ class FunctionCallbackWrapperKotlinIT : BaseOllamaIT() {
|
||||
|
||||
private val MODEL_NAME = "qwen2.5:3b"
|
||||
|
||||
val contextRunner = buildConnectionWithModel(MODEL_NAME).let { baseUrl ->
|
||||
val contextRunner = buildOllamaApiWithModel(MODEL_NAME).let { baseUrl ->
|
||||
ApplicationContextRunner().withPropertyValues(
|
||||
"spring.ai.ollama.baseUrl=$baseUrl",
|
||||
"spring.ai.ollama.chat.options.model=$MODEL_NAME",
|
||||
|
||||
Reference in New Issue
Block a user