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
+
+
+
+