Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce auto-configuration for Cloud Bindings #500

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
<module>spring-ai-spring-boot-starters/spring-ai-starter-neo4j-store</module>
<module>spring-ai-spring-boot-starters/spring-ai-starter-qdrant-store</module>
<module>spring-ai-spring-boot-starters/spring-ai-starter-postgresml-embedding</module>
<module>spring-ai-spring-cloud-bindings</module>
<module>spring-ai-docs</module>
<module>vector-stores/spring-ai-pgvector-store</module>
<module>vector-stores/spring-ai-milvus-store</module>
Expand Down Expand Up @@ -116,6 +117,7 @@
<!-- production dependencies -->
<spring-boot.version>3.2.3</spring-boot.version>
<spring-framework.version>6.1.4</spring-framework.version>
<spring-cloud-bindings.version>2.0.2</spring-cloud-bindings.version>
<stringtemplate.version>4.0.2</stringtemplate.version>
<azure-open-ai-client.version>1.0.0-beta.7</azure-open-ai-client.version>
<jtokkit.version>1.0.0</jtokkit.version>
Expand Down
43 changes: 43 additions & 0 deletions spring-ai-spring-cloud-bindings/pom.xml
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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);
}

}
Original file line number Diff line number Diff line change
@@ -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"));
});
}

}
Original file line number Diff line number Diff line change
@@ -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"));
});
}

}
Original file line number Diff line number Diff line change
@@ -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"));
});
}

}
Original file line number Diff line number Diff line change
@@ -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"));
});
}

}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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();
}

}
Original file line number Diff line number Diff line change
@@ -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();
}

}
Original file line number Diff line number Diff line change
@@ -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();
}

}