From 312156e2cbffb8d1e8ecfb0b4311004dc69a4a14 Mon Sep 17 00:00:00 2001 From: Thomas Vitale Date: Sat, 23 Mar 2024 21:00:34 +0100 Subject: [PATCH] Introduce autoconfiguration for Cloud Bindings Fixes gh-499 Signed-off-by: Thomas Vitale --- pom.xml | 130 +++++++++--------- spring-ai-spring-cloud-bindings/pom.xml | 43 ++++++ .../ai/bindings/BindingsValidator.java | 20 +++ .../ChromaBindingsPropertiesProcessor.java | 40 ++++++ .../OllamaBindingsPropertiesProcessor.java | 34 +++++ .../OpenAiBindingsPropertiesProcessor.java | 35 +++++ .../WeaviateBindingsPropertiesProcessor.java | 36 +++++ .../main/resources/META-INF/spring.factories | 6 + ...hromaBindingsPropertiesProcessorTests.java | 53 +++++++ ...llamaBindingsPropertiesProcessorTests.java | 48 +++++++ ...penAiBindingsPropertiesProcessorTests.java | 50 +++++++ ...viateBindingsPropertiesProcessorTests.java | 51 +++++++ 12 files changed, 482 insertions(+), 64 deletions(-) create mode 100644 spring-ai-spring-cloud-bindings/pom.xml create mode 100644 spring-ai-spring-cloud-bindings/src/main/java/org/springframework/ai/bindings/BindingsValidator.java create mode 100644 spring-ai-spring-cloud-bindings/src/main/java/org/springframework/ai/bindings/ChromaBindingsPropertiesProcessor.java create mode 100644 spring-ai-spring-cloud-bindings/src/main/java/org/springframework/ai/bindings/OllamaBindingsPropertiesProcessor.java create mode 100644 spring-ai-spring-cloud-bindings/src/main/java/org/springframework/ai/bindings/OpenAiBindingsPropertiesProcessor.java create mode 100644 spring-ai-spring-cloud-bindings/src/main/java/org/springframework/ai/bindings/WeaviateBindingsPropertiesProcessor.java create mode 100644 spring-ai-spring-cloud-bindings/src/main/resources/META-INF/spring.factories create mode 100644 spring-ai-spring-cloud-bindings/src/test/java/org/springframework/ai/bindings/ChromaBindingsPropertiesProcessorTests.java create mode 100644 spring-ai-spring-cloud-bindings/src/test/java/org/springframework/ai/bindings/OllamaBindingsPropertiesProcessorTests.java create mode 100644 spring-ai-spring-cloud-bindings/src/test/java/org/springframework/ai/bindings/OpenAiBindingsPropertiesProcessorTests.java create mode 100644 spring-ai-spring-cloud-bindings/src/test/java/org/springframework/ai/bindings/WeaviateBindingsPropertiesProcessorTests.java diff --git a/pom.xml b/pom.xml index 5f583cbb3..eb5b13475 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,5 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 org.springframework.ai @@ -20,6 +20,7 @@ spring-ai-spring-boot-autoconfigure spring-ai-retry spring-ai-spring-boot-testcontainers + spring-ai-spring-cloud-bindings document-readers/pdf-reader document-readers/tika-reader @@ -144,6 +145,7 @@ 9.20.0 4.35.0 2.2.20 + 2.0.2 3.25.2 @@ -303,72 +305,72 @@ - - com.mycila - license-maven-plugin - 4.1 - - - validate - - check - - - - - - the original author or authors. - - 2024 - - - - - -Copyright 2023 - ${year} the original author or authors. + + com.mycila + license-maven-plugin + 4.1 + + + validate + + check + + + + + + the original author or authors. + + 2024 + + + + + + Copyright 2023 - ${year} 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 + 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 + 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. - - - - **/.antlr/** - **/aot.factories - **/.sdkmanrc - **/*.adoc - **/*.puml - **/pom.xml - **/*.properties - **/*.yaml - **/*.yml - **/*.map - **/*.html - **/*.xhtml - **/*.jsp - **/*.js - **/*.css - **/*.txt - **/*.xjb - **/*.ftl - **/*.xsd - **/*.xml - **/*.sh - **/generated/** - **/Dockerfile - - - - - + 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. + + + + **/.antlr/** + **/aot.factories + **/.sdkmanrc + **/*.adoc + **/*.puml + **/pom.xml + **/*.properties + **/*.yaml + **/*.yml + **/*.map + **/*.html + **/*.xhtml + **/*.jsp + **/*.js + **/*.css + **/*.txt + **/*.xjb + **/*.ftl + **/*.xsd + **/*.xml + **/*.sh + **/generated/** + **/Dockerfile + + + + + diff --git a/spring-ai-spring-cloud-bindings/pom.xml b/spring-ai-spring-cloud-bindings/pom.xml new file mode 100644 index 000000000..2bf03d03c --- /dev/null +++ b/spring-ai-spring-cloud-bindings/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + org.springframework.ai + spring-ai + 1.0.0-SNAPSHOT + + spring-ai-spring-cloud-bindings + jar + Spring AI Cloud Bindings + Spring AI Cloud Bindings + https://github.com/spring-projects/spring-ai + + + https://github.com/spring-projects/spring-ai + git://github.com/spring-projects/spring-ai.git + git@github.com:spring-projects/spring-ai.git + + + + + + org.springframework + spring-core + + + + org.springframework.cloud + spring-cloud-bindings + ${spring-cloud-bindings.version} + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + diff --git a/spring-ai-spring-cloud-bindings/src/main/java/org/springframework/ai/bindings/BindingsValidator.java b/spring-ai-spring-cloud-bindings/src/main/java/org/springframework/ai/bindings/BindingsValidator.java new file mode 100644 index 000000000..47af99a19 --- /dev/null +++ b/spring-ai-spring-cloud-bindings/src/main/java/org/springframework/ai/bindings/BindingsValidator.java @@ -0,0 +1,20 @@ +package org.springframework.ai.bindings; + +import org.springframework.core.env.Environment; + +/** + * From https://github.com/spring-cloud/spring-cloud-bindings to switch on/off the + * bindings. + */ +final class BindingsValidator { + + static final String CONFIG_PATH = "spring.ai.cloud.bindings"; + + /** + * Whether the given binding type should be used to contribute properties. + */ + static boolean isTypeEnabled(Environment environment, String type) { + return environment.getProperty("%s.%s.enabled".formatted(CONFIG_PATH, type), Boolean.class, true); + } + +} diff --git a/spring-ai-spring-cloud-bindings/src/main/java/org/springframework/ai/bindings/ChromaBindingsPropertiesProcessor.java b/spring-ai-spring-cloud-bindings/src/main/java/org/springframework/ai/bindings/ChromaBindingsPropertiesProcessor.java new file mode 100644 index 000000000..31db23bab --- /dev/null +++ b/spring-ai-spring-cloud-bindings/src/main/java/org/springframework/ai/bindings/ChromaBindingsPropertiesProcessor.java @@ -0,0 +1,40 @@ +package org.springframework.ai.bindings; + +import org.springframework.cloud.bindings.Binding; +import org.springframework.cloud.bindings.Bindings; +import org.springframework.cloud.bindings.boot.BindingsPropertiesProcessor; +import org.springframework.core.env.Environment; + +import java.net.URI; +import java.util.Map; + +/** + * An implementation of {@link BindingsPropertiesProcessor} that detects {@link Binding}s + * of type: {@value TYPE}. + * + * @author Thomas Vitale + */ +public class ChromaBindingsPropertiesProcessor implements BindingsPropertiesProcessor { + + /** + * The {@link Binding} type that this processor is interested in: {@value}. + **/ + public static final String TYPE = "chroma"; + + @Override + public void process(Environment environment, Bindings bindings, Map properties) { + if (!BindingsValidator.isTypeEnabled(environment, TYPE)) { + return; + } + + bindings.filterBindings(TYPE).forEach(binding -> { + var uri = URI.create(binding.getSecret().get("uri")); + properties.put("spring.ai.vectorstore.chroma.client.host", + "%s://%s".formatted(uri.getScheme(), uri.getHost())); + properties.put("spring.ai.vectorstore.chroma.client.port", String.valueOf(uri.getPort())); + properties.put("spring.ai.vectorstore.chroma.client.username", binding.getSecret().get("username")); + properties.put("spring.ai.vectorstore.chroma.client.password", binding.getSecret().get("password")); + }); + } + +} diff --git a/spring-ai-spring-cloud-bindings/src/main/java/org/springframework/ai/bindings/OllamaBindingsPropertiesProcessor.java b/spring-ai-spring-cloud-bindings/src/main/java/org/springframework/ai/bindings/OllamaBindingsPropertiesProcessor.java new file mode 100644 index 000000000..9f56b732a --- /dev/null +++ b/spring-ai-spring-cloud-bindings/src/main/java/org/springframework/ai/bindings/OllamaBindingsPropertiesProcessor.java @@ -0,0 +1,34 @@ +package org.springframework.ai.bindings; + +import org.springframework.cloud.bindings.Binding; +import org.springframework.cloud.bindings.Bindings; +import org.springframework.cloud.bindings.boot.BindingsPropertiesProcessor; +import org.springframework.core.env.Environment; + +import java.util.Map; + +/** + * An implementation of {@link BindingsPropertiesProcessor} that detects {@link Binding}s + * of type: {@value TYPE}. + * + * @author Thomas Vitale + */ +public class OllamaBindingsPropertiesProcessor implements BindingsPropertiesProcessor { + + /** + * The {@link Binding} type that this processor is interested in: {@value}. + **/ + public static final String TYPE = "ollama"; + + @Override + public void process(Environment environment, Bindings bindings, Map properties) { + if (!BindingsValidator.isTypeEnabled(environment, TYPE)) { + return; + } + + bindings.filterBindings(TYPE).forEach(binding -> { + properties.put("spring.ai.ollama.base-url", binding.getSecret().get("uri")); + }); + } + +} diff --git a/spring-ai-spring-cloud-bindings/src/main/java/org/springframework/ai/bindings/OpenAiBindingsPropertiesProcessor.java b/spring-ai-spring-cloud-bindings/src/main/java/org/springframework/ai/bindings/OpenAiBindingsPropertiesProcessor.java new file mode 100644 index 000000000..8bdbac4f4 --- /dev/null +++ b/spring-ai-spring-cloud-bindings/src/main/java/org/springframework/ai/bindings/OpenAiBindingsPropertiesProcessor.java @@ -0,0 +1,35 @@ +package org.springframework.ai.bindings; + +import org.springframework.cloud.bindings.Binding; +import org.springframework.cloud.bindings.Bindings; +import org.springframework.cloud.bindings.boot.BindingsPropertiesProcessor; +import org.springframework.core.env.Environment; + +import java.util.Map; + +/** + * An implementation of {@link BindingsPropertiesProcessor} that detects {@link Binding}s + * of type: {@value TYPE}. + * + * @author Thomas Vitale + */ +public class OpenAiBindingsPropertiesProcessor implements BindingsPropertiesProcessor { + + /** + * The {@link Binding} type that this processor is interested in: {@value}. + **/ + public static final String TYPE = "openai"; + + @Override + public void process(Environment environment, Bindings bindings, Map properties) { + if (!BindingsValidator.isTypeEnabled(environment, TYPE)) { + return; + } + + bindings.filterBindings(TYPE).forEach(binding -> { + properties.put("spring.ai.openai.api-key", binding.getSecret().get("api-key")); + properties.put("spring.ai.openai.base-url", binding.getSecret().get("uri")); + }); + } + +} diff --git a/spring-ai-spring-cloud-bindings/src/main/java/org/springframework/ai/bindings/WeaviateBindingsPropertiesProcessor.java b/spring-ai-spring-cloud-bindings/src/main/java/org/springframework/ai/bindings/WeaviateBindingsPropertiesProcessor.java new file mode 100644 index 000000000..ebab016eb --- /dev/null +++ b/spring-ai-spring-cloud-bindings/src/main/java/org/springframework/ai/bindings/WeaviateBindingsPropertiesProcessor.java @@ -0,0 +1,36 @@ +package org.springframework.ai.bindings; + +import org.springframework.cloud.bindings.Binding; +import org.springframework.cloud.bindings.Bindings; +import org.springframework.cloud.bindings.boot.BindingsPropertiesProcessor; +import org.springframework.core.env.Environment; + +import java.net.URI; +import java.util.Map; + +/** + * An implementation of {@link BindingsPropertiesProcessor} that detects {@link Binding}s + * of type: {@value TYPE}. + */ +public class WeaviateBindingsPropertiesProcessor implements BindingsPropertiesProcessor { + + /** + * The {@link Binding} type that this processor is interested in: {@value}. + **/ + public static final String TYPE = "weaviate"; + + @Override + public void process(Environment environment, Bindings bindings, Map properties) { + if (!BindingsValidator.isTypeEnabled(environment, TYPE)) { + return; + } + + bindings.filterBindings(TYPE).forEach(binding -> { + var uri = URI.create(binding.getSecret().get("uri")); + properties.put("spring.ai.vectorstore.weaviate.scheme", uri.getScheme()); + properties.put("spring.ai.vectorstore.weaviate.host", "%s:%s".formatted(uri.getHost(), uri.getPort())); + properties.put("spring.ai.vectorstore.weaviate.api-key", binding.getSecret().get("api-key")); + }); + } + +} diff --git a/spring-ai-spring-cloud-bindings/src/main/resources/META-INF/spring.factories b/spring-ai-spring-cloud-bindings/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..72df6ee85 --- /dev/null +++ b/spring-ai-spring-cloud-bindings/src/main/resources/META-INF/spring.factories @@ -0,0 +1,6 @@ +# Binding Properties Factories +org.springframework.cloud.bindings.boot.BindingsPropertiesProcessor=\ +org.springframework.ai.bindings.ChromaBindingsPropertiesProcessor,\ +org.springframework.ai.bindings.OllamaBindingsPropertiesProcessor,\ +org.springframework.ai.bindings.OpenAiBindingsPropertiesProcessor,\ +org.springframework.ai.bindings.WeaviateBindingsPropertiesProcessor diff --git a/spring-ai-spring-cloud-bindings/src/test/java/org/springframework/ai/bindings/ChromaBindingsPropertiesProcessorTests.java b/spring-ai-spring-cloud-bindings/src/test/java/org/springframework/ai/bindings/ChromaBindingsPropertiesProcessorTests.java new file mode 100644 index 000000000..aba20fe25 --- /dev/null +++ b/spring-ai-spring-cloud-bindings/src/test/java/org/springframework/ai/bindings/ChromaBindingsPropertiesProcessorTests.java @@ -0,0 +1,53 @@ +package org.springframework.ai.bindings; + +import org.junit.jupiter.api.Test; +import org.springframework.cloud.bindings.Binding; +import org.springframework.cloud.bindings.Bindings; +import org.springframework.mock.env.MockEnvironment; + +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.ai.bindings.BindingsValidator.CONFIG_PATH; + +/** + * Unit tests for {@link ChromaBindingsPropertiesProcessor}. + * + * @author Thomas Vitale + */ +class ChromaBindingsPropertiesProcessorTests { + + private final Bindings bindings = new Bindings(new Binding("test-name", Paths.get("test-path"), + // @formatter:off + Map.of( + Binding.TYPE, ChromaBindingsPropertiesProcessor.TYPE, + "uri", "https://example.net:8000", + "username", "itsme", + "password", "youknowit" + ))); + // @formatter:on + + private final MockEnvironment environment = new MockEnvironment(); + + private final Map properties = new HashMap<>(); + + @Test + void propertiesAreContributed() { + new ChromaBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties).containsEntry("spring.ai.vectorstore.chroma.client.host", "https://example.net"); + assertThat(properties).containsEntry("spring.ai.vectorstore.chroma.client.port", "8000"); + assertThat(properties).containsEntry("spring.ai.vectorstore.chroma.client.username", "itsme"); + assertThat(properties).containsEntry("spring.ai.vectorstore.chroma.client.password", "youknowit"); + } + + @Test + void whenDisabledThenPropertiesAreNotContributed() { + environment.setProperty("%s.chroma.enabled".formatted(CONFIG_PATH), "false"); + + new ChromaBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties).isEmpty(); + } + +} diff --git a/spring-ai-spring-cloud-bindings/src/test/java/org/springframework/ai/bindings/OllamaBindingsPropertiesProcessorTests.java b/spring-ai-spring-cloud-bindings/src/test/java/org/springframework/ai/bindings/OllamaBindingsPropertiesProcessorTests.java new file mode 100644 index 000000000..9b44248bf --- /dev/null +++ b/spring-ai-spring-cloud-bindings/src/test/java/org/springframework/ai/bindings/OllamaBindingsPropertiesProcessorTests.java @@ -0,0 +1,48 @@ +package org.springframework.ai.bindings; + +import org.junit.jupiter.api.Test; +import org.springframework.cloud.bindings.Binding; +import org.springframework.cloud.bindings.Bindings; +import org.springframework.mock.env.MockEnvironment; + +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.ai.bindings.BindingsValidator.CONFIG_PATH; + +/** + * Unit tests for {@link OllamaBindingsPropertiesProcessor}. + * + * @author Thomas Vitale + */ +class OllamaBindingsPropertiesProcessorTests { + + private final Bindings bindings = new Bindings(new Binding("test-name", Paths.get("test-path"), + // @formatter:off + Map.of( + Binding.TYPE, OllamaBindingsPropertiesProcessor.TYPE, + "uri", "https://example.net/ollama:11434" + ))); + // @formatter:on + + private final MockEnvironment environment = new MockEnvironment(); + + private final Map properties = new HashMap<>(); + + @Test + void propertiesAreContributed() { + new OllamaBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties).containsEntry("spring.ai.ollama.base-url", "https://example.net/ollama:11434"); + } + + @Test + void whenDisabledThenPropertiesAreNotContributed() { + environment.setProperty("%s.ollama.enabled".formatted(CONFIG_PATH), "false"); + + new OllamaBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties).isEmpty(); + } + +} diff --git a/spring-ai-spring-cloud-bindings/src/test/java/org/springframework/ai/bindings/OpenAiBindingsPropertiesProcessorTests.java b/spring-ai-spring-cloud-bindings/src/test/java/org/springframework/ai/bindings/OpenAiBindingsPropertiesProcessorTests.java new file mode 100644 index 000000000..f41b38b41 --- /dev/null +++ b/spring-ai-spring-cloud-bindings/src/test/java/org/springframework/ai/bindings/OpenAiBindingsPropertiesProcessorTests.java @@ -0,0 +1,50 @@ +package org.springframework.ai.bindings; + +import org.junit.jupiter.api.Test; +import org.springframework.cloud.bindings.Binding; +import org.springframework.cloud.bindings.Bindings; +import org.springframework.mock.env.MockEnvironment; + +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.ai.bindings.BindingsValidator.CONFIG_PATH; + +/** + * Unit tests for {@link OpenAiBindingsPropertiesProcessor}. + * + * @author Thomas Vitale + */ +class OpenAiBindingsPropertiesProcessorTests { + + private final Bindings bindings = new Bindings(new Binding("test-name", Paths.get("test-path"), + // @formatter:off + Map.of( + Binding.TYPE, OpenAiBindingsPropertiesProcessor.TYPE, + "api-key", "demo", + "uri", "https://my.openai.example.net" + ))); + // @formatter:on + + private final MockEnvironment environment = new MockEnvironment(); + + private final Map properties = new HashMap<>(); + + @Test + void propertiesAreContributed() { + new OpenAiBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties).containsEntry("spring.ai.openai.api-key", "demo"); + assertThat(properties).containsEntry("spring.ai.openai.base-url", "https://my.openai.example.net"); + } + + @Test + void whenDisabledThenPropertiesAreNotContributed() { + environment.setProperty("%s.openai.enabled".formatted(CONFIG_PATH), "false"); + + new OpenAiBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties).isEmpty(); + } + +} diff --git a/spring-ai-spring-cloud-bindings/src/test/java/org/springframework/ai/bindings/WeaviateBindingsPropertiesProcessorTests.java b/spring-ai-spring-cloud-bindings/src/test/java/org/springframework/ai/bindings/WeaviateBindingsPropertiesProcessorTests.java new file mode 100644 index 000000000..0981ddfe6 --- /dev/null +++ b/spring-ai-spring-cloud-bindings/src/test/java/org/springframework/ai/bindings/WeaviateBindingsPropertiesProcessorTests.java @@ -0,0 +1,51 @@ +package org.springframework.ai.bindings; + +import org.junit.jupiter.api.Test; +import org.springframework.cloud.bindings.Binding; +import org.springframework.cloud.bindings.Bindings; +import org.springframework.mock.env.MockEnvironment; + +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.ai.bindings.BindingsValidator.CONFIG_PATH; + +/** + * Unit tests for {@link WeaviateBindingsPropertiesProcessor}. + * + * @author Thomas Vitale + */ +class WeaviateBindingsPropertiesProcessorTests { + + private final Bindings bindings = new Bindings(new Binding("test-name", Paths.get("test-path"), + // @formatter:off + Map.of( + Binding.TYPE, WeaviateBindingsPropertiesProcessor.TYPE, + "uri", "https://example.net:8000", + "api-key", "demo" + ))); + // @formatter:on + + private final MockEnvironment environment = new MockEnvironment(); + + private final Map properties = new HashMap<>(); + + @Test + void propertiesAreContributed() { + new WeaviateBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties).containsEntry("spring.ai.vectorstore.weaviate.scheme", "https"); + assertThat(properties).containsEntry("spring.ai.vectorstore.weaviate.host", "example.net:8000"); + assertThat(properties).containsEntry("spring.ai.vectorstore.weaviate.api-key", "demo"); + } + + @Test + void whenDisabledThenPropertiesAreNotContributed() { + environment.setProperty("%s.weaviate.enabled".formatted(CONFIG_PATH), "false"); + + new WeaviateBindingsPropertiesProcessor().process(environment, bindings, properties); + assertThat(properties).isEmpty(); + } + +}