From 6bdea65cd50759249db509fbaa3f5393be741f95 Mon Sep 17 00:00:00 2001 From: Tommy Ludwig <8924140+shakuzen@users.noreply.github.com> Date: Thu, 16 Nov 2023 23:23:20 +0900 Subject: [PATCH] Auto-configuration and starter for Ollama - Adds auto-configuration and a starter for Ollama. - Add testconteiner orca-mini integration test. - Improve code style and documentation. Resolves #68, Resolves #69 --- pom.xml | 1 + spring-ai-ollama/README.md | 53 +++++++++++- spring-ai-spring-boot-autoconfigure/pom.xml | 7 ++ .../ollama/OllamaAutoConfiguration.java | 37 +++++++++ .../ollama/OllamaProperties.java | 52 ++++++++++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../ollama/OllamaAutoConfigurationIT.java | 74 +++++++++++++++++ .../ollama/OllamaAutoConfigurationTests.java | 81 +++++++++++++++++++ .../spring-ai-starter-ollama/pom.xml | 51 ++++++++++++ 9 files changed, 354 insertions(+), 3 deletions(-) create mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/OllamaAutoConfiguration.java create mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/OllamaProperties.java create mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/OllamaAutoConfigurationIT.java create mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/OllamaAutoConfigurationTests.java create mode 100644 spring-ai-spring-boot-starters/spring-ai-starter-ollama/pom.xml diff --git a/pom.xml b/pom.xml index bf8617415..cc5c86079 100644 --- a/pom.xml +++ b/pom.xml @@ -22,6 +22,7 @@ spring-ai-spring-boot-autoconfigure spring-ai-spring-boot-starters/spring-ai-starter-openai spring-ai-spring-boot-starters/spring-ai-starter-azure-openai + spring-ai-spring-boot-starters/spring-ai-starter-ollama spring-ai-spring-boot-starters/spring-ai-starter-transformers-embedding spring-ai-docs vector-stores/spring-ai-pgvector-store diff --git a/spring-ai-ollama/README.md b/spring-ai-ollama/README.md index 21a76267c..dc99fd278 100644 --- a/spring-ai-ollama/README.md +++ b/spring-ai-ollama/README.md @@ -1,9 +1,56 @@ -## Ollama +# Ollama -Ollama lets you ge tup an running with large language models locally +Ollama lets you get up and running with large language models locally. Refer to the official [README](https://github.com/jmorganca/ollama) to get started. Note, installing `ollama run llama2` will download a 4GB docker image. -You can run the disabled test in `OllamaClientTests.java` to kick the tires. \ No newline at end of file +You can run the disabled test in `OllamaClientTests.java` to kick the tires. + +## How to use + +Add the `spring-ai-ollama` dependency to your project's pom: + +```xml + + org.springframework.experimental.ai + spring-ai-ollama + 0.7.1-SNAPSHOT + +``` + +then create an client and use generate response: + +```java +var ollamaClient = new OllamaClient("http://127.0.0.1:11434", "llama2", + ollamaResult -> { + if (ollamaResult.getDone()) { + .... + } + }); + +AiResponse aiResponse = ollamaClient.generate(new Prompt("Hello")); +``` + +### Spring Boot Starter + +For convenience you can opt for the Ollama Boot starter. +For this add the following dependency: + +```xml + + org.springframework.experimental.ai + spring-ai-ollama-spring-boot-starter + 0.7.1-SNAPSHOT + +``` + +and use the `spring.ai.ollama.*` properties to configure it if you want to use something other than the default values. + +The complete list of supported properties are: + +| Property | Description | Default | +| -------- | ----- | ----- | +| spring.ai.ollama.base-url | Base URL where Ollama API server is running. | `http://localhost:11434` | +| spring.ai.ollama.model | Language model to use. | `llama2` | diff --git a/spring-ai-spring-boot-autoconfigure/pom.xml b/spring-ai-spring-boot-autoconfigure/pom.xml index 36d06b91c..8bdbfa1b6 100644 --- a/spring-ai-spring-boot-autoconfigure/pom.xml +++ b/spring-ai-spring-boot-autoconfigure/pom.xml @@ -49,6 +49,13 @@ true + + org.springframework.experimental.ai + spring-ai-ollama + ${project.parent.version} + true + + org.springframework.experimental.ai diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/OllamaAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/OllamaAutoConfiguration.java new file mode 100644 index 000000000..6bbd53635 --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/OllamaAutoConfiguration.java @@ -0,0 +1,37 @@ +/* + * Copyright 2023-2023 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.autoconfigure.ollama; + +import org.springframework.ai.ollama.client.OllamaClient; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +@AutoConfiguration +@ConditionalOnClass(OllamaClient.class) +@EnableConfigurationProperties(OllamaProperties.class) +public class OllamaAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public OllamaClient ollamaClient(OllamaProperties properties) { + return new OllamaClient(properties.getBaseUrl(), properties.getModel()); + } + +} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/OllamaProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/OllamaProperties.java new file mode 100644 index 000000000..c72fdb5b8 --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/OllamaProperties.java @@ -0,0 +1,52 @@ +/* + * Copyright 2023-2023 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.autoconfigure.ollama; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(OllamaProperties.CONFIG_PREFIX) +public class OllamaProperties { + + public static final String CONFIG_PREFIX = "spring.ai.ollama"; + + /** + * Base URL where Ollama API server is running. + */ + private String baseUrl = "http://localhost:11434"; + + /** + * Language model to use. + */ + private String model = "llama2"; + + public String getBaseUrl() { + return baseUrl; + } + + public void setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + } + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + +} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 513d81bdc..e69af94f1 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,3 +1,4 @@ +org.springframework.ai.autoconfigure.ollama.OllamaAutoConfiguration org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiAutoConfiguration org.springframework.ai.autoconfigure.vectorstore.pgvector.PgVectorStoreAutoConfiguration diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/OllamaAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/OllamaAutoConfigurationIT.java new file mode 100644 index 000000000..8535b8247 --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/OllamaAutoConfigurationIT.java @@ -0,0 +1,74 @@ +/* + * Copyright 2023-2023 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.autoconfigure.ollama; + +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.ai.ollama.client.OllamaClient; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.http.HttpEntity; +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.Assertions.assertThat; + +@Disabled("As it downloads the 3GB 'orca-mini' it can take couple of minutes to initialize.") +@Testcontainers +public class OllamaAutoConfigurationIT { + + private static final Log logger = LogFactory.getLog(OllamaAutoConfigurationIT.class); + + @Container + static GenericContainer ollamaContainer = new GenericContainer<>("ollama/ollama").withExposedPorts(11434); + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("spring.ai.ollama.baseUrl=http://localhost:" + ollamaContainer.getMappedPort(11434), + "spring.ai.ollama.model=orca-mini") + .withConfiguration(AutoConfigurations.of(OllamaAutoConfiguration.class)); + + @BeforeAll + public static void beforeAll() { + logger.info("Start pulling the 'orca-mini' model (3GB) ... would take several minutes ..."); + new RestTemplate().postForLocation("http://localhost:{port}/api/pull", + new HttpEntity<>(Map.of("name", "orca-mini")), ollamaContainer.getMappedPort(11434)); + logger.info("orca-mini pulling competed!"); + } + + @Test + void generate() { + contextRunner.run(context -> { + OllamaClient client = context.getBean(OllamaClient.class); + assertThat(client.getBaseUrl()).isEqualTo("http://localhost:" + ollamaContainer.getMappedPort(11434)); + assertThat(client.getModel()).isEqualTo("orca-mini"); + + String response = client.generate("Hello"); + + assertThat(response).isNotEmpty(); + logger.info("Response: " + response); + }); + } + +} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/OllamaAutoConfigurationTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/OllamaAutoConfigurationTests.java new file mode 100644 index 000000000..73f42221e --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/OllamaAutoConfigurationTests.java @@ -0,0 +1,81 @@ +/* + * Copyright 2023-2023 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.autoconfigure.ollama; + +import org.junit.jupiter.api.Test; + +import org.springframework.ai.ollama.client.OllamaClient; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OllamaAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(OllamaAutoConfiguration.class)); + + @Test + void defaults() { + contextRunner.run(context -> { + OllamaProperties properties = context.getBean(OllamaProperties.class); + assertThat(properties.getBaseUrl()).isEqualTo("http://localhost:11434"); + assertThat(properties.getModel()).isEqualTo("llama2"); + + OllamaClient client = context.getBean(OllamaClient.class); + assertThat(client.getBaseUrl()).isEqualTo("http://localhost:11434"); + assertThat(client.getModel()).isEqualTo("llama2"); + }); + } + + @Test + void overrideProperties() { + contextRunner + .withPropertyValues("spring.ai.ollama.base-url=http://localhost:8080", "spring.ai.ollama.model=myModel") + .run(context -> { + OllamaProperties properties = context.getBean(OllamaProperties.class); + assertThat(properties.getBaseUrl()).isEqualTo("http://localhost:8080"); + assertThat(properties.getModel()).isEqualTo("myModel"); + + OllamaClient client = context.getBean(OllamaClient.class); + assertThat(client.getBaseUrl()).isEqualTo("http://localhost:8080"); + assertThat(client.getModel()).isEqualTo("myModel"); + }); + } + + @Test + void customConfig() { + contextRunner.withUserConfiguration(CustomConfig.class).run(context -> { + OllamaClient ollamaClient = context.getBean(OllamaClient.class); + assertThat(ollamaClient.getBaseUrl()).isEqualTo("http://localhost:8080"); + assertThat(ollamaClient.getModel()).isEqualTo("myModel"); + }); + } + + @Configuration(proxyBeanMethods = false) + static class CustomConfig { + + @Bean + OllamaClient myClient() { + return new OllamaClient("http://localhost:8080", "myModel"); + } + + } + +} diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-ollama/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-ollama/pom.xml new file mode 100644 index 000000000..970dbfcc4 --- /dev/null +++ b/spring-ai-spring-boot-starters/spring-ai-starter-ollama/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + org.springframework.experimental.ai + spring-ai + 0.7.1-SNAPSHOT + ../../pom.xml + + spring-ai-ollama-spring-boot-starter + jar + Spring AI Starter - Ollama + Spring AI Ollama Auto Configuration + https://github.com/spring-projects-experimental/spring-ai + + + https://github.com/spring-projects-experimental/spring-ai + git://github.com/spring-projects-experimental/spring-ai.git + git@github.com:spring-projects-experimental/spring-ai.git + + + + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.experimental.ai + spring-ai-spring-boot-autoconfigure + ${project.parent.version} + + + + org.springframework.experimental.ai + spring-ai-ollama + ${project.parent.version} + + + + + org.springframework.boot + spring-boot-starter-test + test + + + +