Introduce autoconfiguration for Cloud Bindings

Fixes gh-499

Signed-off-by: Thomas Vitale <ThomasVitale@users.noreply.github.com>
This commit is contained in:
Thomas Vitale
2024-03-23 21:00:34 +01:00
committed by Mark Pollack
parent ac518df4db
commit 312156e2cb
12 changed files with 482 additions and 64 deletions

130
pom.xml
View File

@@ -1,5 +1,5 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
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">
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">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.ai</groupId>
@@ -20,6 +20,7 @@
<module>spring-ai-spring-boot-autoconfigure</module>
<module>spring-ai-retry</module>
<module>spring-ai-spring-boot-testcontainers</module>
<module>spring-ai-spring-cloud-bindings</module>
<module>document-readers/pdf-reader</module>
<module>document-readers/tika-reader</module>
@@ -144,6 +145,7 @@
<ibm.sdk.version>9.20.0</ibm.sdk.version>
<jsonschema.version>4.35.0</jsonschema.version>
<swagger-annotations.version>2.2.20</swagger-annotations.version>
<spring-cloud-bindings.version>2.0.2</spring-cloud-bindings.version>
<!-- Protobuf -->
<protobuf-java.version>3.25.2</protobuf-java.version>
@@ -303,72 +305,72 @@
</activation>
<build>
<plugins>
<plugin>
<groupId>com.mycila</groupId>
<artifactId>license-maven-plugin</artifactId>
<version>4.1</version>
<executions>
<execution>
<phase>validate</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
<configuration>
<properties>
<owner>the original author or authors.</owner>
<email/>
<year>2024</year>
</properties>
<licenseSets>
<licenseSet>
<inlineHeader>
<!-- @formatter:off -->
Copyright 2023 - ${year} the original author or authors.
<plugin>
<groupId>com.mycila</groupId>
<artifactId>license-maven-plugin</artifactId>
<version>4.1</version>
<executions>
<execution>
<phase>validate</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
<configuration>
<properties>
<owner>the original author or authors.</owner>
<email/>
<year>2024</year>
</properties>
<licenseSets>
<licenseSet>
<inlineHeader>
<!-- @formatter:off -->
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.
<!-- @formatter:on -->
</inlineHeader>
<excludes>
<exclude>**/.antlr/**</exclude>
<exclude>**/aot.factories</exclude>
<exclude>**/.sdkmanrc</exclude>
<exclude>**/*.adoc</exclude>
<exclude>**/*.puml</exclude>
<exclude>**/pom.xml</exclude>
<exclude>**/*.properties</exclude>
<exclude>**/*.yaml</exclude>
<exclude>**/*.yml</exclude>
<exclude>**/*.map</exclude>
<exclude>**/*.html</exclude>
<exclude>**/*.xhtml</exclude>
<exclude>**/*.jsp</exclude>
<exclude>**/*.js</exclude>
<exclude>**/*.css</exclude>
<exclude>**/*.txt</exclude>
<exclude>**/*.xjb</exclude>
<exclude>**/*.ftl</exclude>
<exclude>**/*.xsd</exclude>
<exclude>**/*.xml</exclude>
<exclude>**/*.sh</exclude>
<exclude>**/generated/**</exclude>
<exclude>**/Dockerfile</exclude>
</excludes>
</licenseSet>
</licenseSets>
</configuration>
</plugin>
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.
<!-- @formatter:on -->
</inlineHeader>
<excludes>
<exclude>**/.antlr/**</exclude>
<exclude>**/aot.factories</exclude>
<exclude>**/.sdkmanrc</exclude>
<exclude>**/*.adoc</exclude>
<exclude>**/*.puml</exclude>
<exclude>**/pom.xml</exclude>
<exclude>**/*.properties</exclude>
<exclude>**/*.yaml</exclude>
<exclude>**/*.yml</exclude>
<exclude>**/*.map</exclude>
<exclude>**/*.html</exclude>
<exclude>**/*.xhtml</exclude>
<exclude>**/*.jsp</exclude>
<exclude>**/*.js</exclude>
<exclude>**/*.css</exclude>
<exclude>**/*.txt</exclude>
<exclude>**/*.xjb</exclude>
<exclude>**/*.ftl</exclude>
<exclude>**/*.xsd</exclude>
<exclude>**/*.xml</exclude>
<exclude>**/*.sh</exclude>
<exclude>**/generated/**</exclude>
<exclude>**/Dockerfile</exclude>
</excludes>
</licenseSet>
</licenseSets>
</configuration>
</plugin>
</plugins>
</build>

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>spring-ai-spring-cloud-bindings</artifactId>
<packaging>jar</packaging>
<name>Spring AI Cloud Bindings</name>
<description>Spring AI Cloud Bindings</description>
<url>https://github.com/spring-projects/spring-ai</url>
<scm>
<url>https://github.com/spring-projects/spring-ai</url>
<connection>git://github.com/spring-projects/spring-ai.git</connection>
<developerConnection>git@github.com:spring-projects/spring-ai.git</developerConnection>
</scm>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-bindings</artifactId>
<version>${spring-cloud-bindings.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -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);
}
}

View File

@@ -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<String, Object> 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"));
});
}
}

View File

@@ -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<String, Object> properties) {
if (!BindingsValidator.isTypeEnabled(environment, TYPE)) {
return;
}
bindings.filterBindings(TYPE).forEach(binding -> {
properties.put("spring.ai.ollama.base-url", binding.getSecret().get("uri"));
});
}
}

View File

@@ -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<String, Object> 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"));
});
}
}

View File

@@ -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<String, Object> 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"));
});
}
}

View File

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

View File

@@ -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<String, Object> 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();
}
}

View File

@@ -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<String, Object> 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();
}
}

View File

@@ -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<String, Object> 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();
}
}

View File

@@ -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<String, Object> 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();
}
}