Add OCI GenAI embedding model support

This commit introduces support for Oracle Cloud Infrastructure (OCI)
GenAI embedding models in Spring AI. It includes:

* New OCIEmbeddingModel class for interacting with OCI GenAI API
* Auto-configuration for easy setup and integration
* Properties for configuring OCI connection and embedding options
* Documentation updates explaining usage and configuration
* Integration tests to verify functionality

Signed-off-by: Anders Swanson <anders.swanson@oracle.com>
This commit is contained in:
Anders Swanson
2024-08-20 12:13:24 -07:00
committed by Mark Pollack
parent 5da44c4b45
commit ccf190c77c
20 changed files with 1158 additions and 2 deletions

View File

@@ -0,0 +1 @@
[Oracle Cloud Infrastructure GenAI Documentation](https://docs.oracle.com/en-us/iaas/Content/generative-ai/overview.htm)

View File

@@ -0,0 +1,69 @@
<?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>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>spring-ai-oci-genai</artifactId>
<packaging>jar</packaging>
<name>Spring AI Model - OCI GenAI</name>
<description>OCI GenAI models support</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>
<!-- production dependencies -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-core</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>com.oracle.oci.sdk</groupId>
<artifactId>oci-java-sdk-shaded-full</artifactId>
<version>${oci-sdk-version}</version>
</dependency>
<dependency>
<groupId>com.oracle.oci.sdk</groupId>
<artifactId>oci-java-sdk-addons-oke-workload-identity</artifactId>
<version>${oci-sdk-version}</version>
</dependency>
<!-- NOTE: Required only by the @ConstructorBinding. -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- test dependencies -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-test</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,177 @@
/*
* Copyright 2024 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.oci;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import com.oracle.bmc.generativeaiinference.GenerativeAiInference;
import com.oracle.bmc.generativeaiinference.model.DedicatedServingMode;
import com.oracle.bmc.generativeaiinference.model.EmbedTextDetails;
import com.oracle.bmc.generativeaiinference.model.EmbedTextResult;
import com.oracle.bmc.generativeaiinference.model.OnDemandServingMode;
import com.oracle.bmc.generativeaiinference.model.ServingMode;
import com.oracle.bmc.generativeaiinference.requests.EmbedTextRequest;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.ai.chat.metadata.EmptyUsage;
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.AbstractEmbeddingModel;
import org.springframework.ai.embedding.Embedding;
import org.springframework.ai.embedding.EmbeddingOptions;
import org.springframework.ai.embedding.EmbeddingRequest;
import org.springframework.ai.embedding.EmbeddingResponse;
import org.springframework.ai.embedding.EmbeddingResponseMetadata;
import org.springframework.ai.embedding.observation.DefaultEmbeddingModelObservationConvention;
import org.springframework.ai.embedding.observation.EmbeddingModelObservationContext;
import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention;
import org.springframework.ai.embedding.observation.EmbeddingModelObservationDocumentation;
import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.ai.observation.conventions.AiProvider;
import org.springframework.util.Assert;
/**
* {@link org.springframework.ai.embedding.EmbeddingModel} implementation that uses the
* OCI GenAI Embedding API.
*
* @author Anders Swanson
* @since 1.0.0
*/
public class OCIEmbeddingModel extends AbstractEmbeddingModel {
// The OCI GenAI API has a batch size of 96 for embed text requests.
private static final int EMBEDTEXT_BATCH_SIZE = 96;
private static final EmbeddingModelObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultEmbeddingModelObservationConvention();
private final GenerativeAiInference genAi;
private final OCIEmbeddingOptions options;
private final ObservationRegistry observationRegistry;
private final EmbeddingModelObservationConvention observationConvention = DEFAULT_OBSERVATION_CONVENTION;
public OCIEmbeddingModel(GenerativeAiInference genAi, OCIEmbeddingOptions options) {
this(genAi, options, ObservationRegistry.NOOP);
}
public OCIEmbeddingModel(GenerativeAiInference genAi, OCIEmbeddingOptions options,
ObservationRegistry observationRegistry) {
Assert.notNull(genAi, "com.oracle.bmc.generativeaiinference.GenerativeAiInferenceClient must not be null");
Assert.notNull(options, "options must not be null");
Assert.notNull(observationRegistry, "observationRegistry must not be null");
this.genAi = genAi;
this.options = options;
this.observationRegistry = observationRegistry;
}
@Override
public EmbeddingResponse call(EmbeddingRequest request) {
Assert.notEmpty(request.getInstructions(), "At least one text is required!");
OCIEmbeddingOptions runtimeOptions = mergeOptions(request.getOptions(), options);
List<EmbedTextRequest> embedTextRequests = createRequests(request.getInstructions(), runtimeOptions);
EmbeddingModelObservationContext context = EmbeddingModelObservationContext.builder()
.embeddingRequest(request)
.provider(AiProvider.OCI_GENAI.value())
.requestOptions(runtimeOptions)
.build();
return EmbeddingModelObservationDocumentation.EMBEDDING_MODEL_OPERATION
.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> context,
this.observationRegistry)
.observe(() -> embedAllWithContext(embedTextRequests, context));
}
@Override
public float[] embed(Document document) {
return embed(document.getContent());
}
private EmbeddingResponse embedAllWithContext(List<EmbedTextRequest> embedTextRequests,
EmbeddingModelObservationContext context) {
String modelId = null;
AtomicInteger index = new AtomicInteger(0);
List<Embedding> embeddings = new ArrayList<>();
for (EmbedTextRequest embedTextRequest : embedTextRequests) {
EmbedTextResult embedTextResult = genAi.embedText(embedTextRequest).getEmbedTextResult();
if (modelId == null) {
modelId = embedTextResult.getModelId();
}
for (List<Float> e : embedTextResult.getEmbeddings()) {
float[] data = toFloats(e);
embeddings.add(new Embedding(data, index.getAndIncrement()));
}
}
EmbeddingResponseMetadata metadata = new EmbeddingResponseMetadata();
metadata.setModel(modelId);
metadata.setUsage(new EmptyUsage());
EmbeddingResponse embeddingResponse = new EmbeddingResponse(embeddings, metadata);
context.setResponse(embeddingResponse);
return embeddingResponse;
}
private ServingMode servingMode(OCIEmbeddingOptions embeddingOptions) {
return switch (embeddingOptions.getServingMode()) {
case "dedicated" -> DedicatedServingMode.builder().endpointId(embeddingOptions.getModel()).build();
case "on-demand" -> OnDemandServingMode.builder().modelId(embeddingOptions.getModel()).build();
default -> throw new IllegalArgumentException(
"unknown serving mode for OCI embedding model: " + embeddingOptions.getServingMode());
};
}
private List<EmbedTextRequest> createRequests(List<String> inputs, OCIEmbeddingOptions embeddingOptions) {
int size = inputs.size();
List<EmbedTextRequest> requests = new ArrayList<>();
for (int i = 0; i < inputs.size(); i += EMBEDTEXT_BATCH_SIZE) {
List<String> batch = inputs.subList(i, Math.min(i + EMBEDTEXT_BATCH_SIZE, size));
requests.add(createRequest(batch, embeddingOptions));
}
return requests;
}
private EmbedTextRequest createRequest(List<String> inputs, OCIEmbeddingOptions embeddingOptions) {
EmbedTextDetails embedTextDetails = EmbedTextDetails.builder()
.servingMode(servingMode(embeddingOptions))
.compartmentId(embeddingOptions.getCompartment())
.inputs(inputs)
.truncate(Objects.requireNonNullElse(embeddingOptions.getTruncate(), EmbedTextDetails.Truncate.End))
.build();
return EmbedTextRequest.builder().embedTextDetails(embedTextDetails).build();
}
private OCIEmbeddingOptions mergeOptions(EmbeddingOptions embeddingOptions, OCIEmbeddingOptions defaultOptions) {
if (embeddingOptions instanceof OCIEmbeddingOptions) {
OCIEmbeddingOptions dynamicOptions = ModelOptionsUtils.merge(embeddingOptions, defaultOptions,
OCIEmbeddingOptions.class);
if (dynamicOptions != null) {
return dynamicOptions;
}
}
return defaultOptions;
}
private float[] toFloats(List<Float> embedding) {
float[] floats = new float[embedding.size()];
for (int i = 0; i < embedding.size(); i++) {
floats[i] = embedding.get(i);
}
return floats;
}
}

View File

@@ -0,0 +1,114 @@
/*
* Copyright 2024 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.oci;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.oracle.bmc.generativeaiinference.model.EmbedTextDetails;
import org.springframework.ai.embedding.EmbeddingOptions;
/**
* The configuration information for OCI embedding requests
*
* @author Anders Swanson
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public class OCIEmbeddingOptions implements EmbeddingOptions {
private @JsonProperty("model") String model;
private @JsonProperty("compartment") String compartment;
private @JsonProperty("servingMode") String servingMode;
private @JsonProperty("truncate") EmbedTextDetails.Truncate truncate;
public static Builder builder() {
return new Builder();
}
public static class Builder {
private final OCIEmbeddingOptions options = new OCIEmbeddingOptions();
public Builder withModel(String model) {
this.options.setModel(model);
return this;
}
public Builder withCompartment(String compartment) {
this.options.setCompartment(compartment);
return this;
}
public Builder withServingMode(String servingMode) {
this.options.setServingMode(servingMode);
return this;
}
public Builder withTruncate(EmbedTextDetails.Truncate truncate) {
this.options.truncate = truncate;
return this;
}
public OCIEmbeddingOptions build() {
return this.options;
}
}
public String getModel() {
return this.model;
}
/**
* Not used by OCI GenAI.
* @return null
*/
@Override
public Integer getDimensions() {
return null;
}
public void setModel(String model) {
this.model = model;
}
public String getCompartment() {
return compartment;
}
public void setCompartment(String compartment) {
this.compartment = compartment;
}
public String getServingMode() {
return servingMode;
}
public void setServingMode(String servingMode) {
this.servingMode = servingMode;
}
public EmbedTextDetails.Truncate getTruncate() {
return truncate;
}
public void setTruncate(EmbedTextDetails.Truncate truncate) {
this.truncate = truncate;
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2024 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.oci;
import java.io.IOException;
import java.nio.file.Paths;
import com.oracle.bmc.Region;
import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider;
import com.oracle.bmc.generativeaiinference.GenerativeAiInferenceClient;
public class BaseEmbeddingModelTest {
public static final String OCI_COMPARTMENT_ID_KEY = "OCI_COMPARTMENT_ID";
public static final String EMBEDDING_MODEL_V2 = "cohere.embed-english-light-v2.0";
public static final String EMBEDDING_MODEL_V3 = "cohere.embed-english-v3.0";
private static final String CONFIG_FILE = Paths.get(System.getProperty("user.home"), ".oci", "config").toString();
private static final String PROFILE = "DEFAULT";
private static final String REGION = "us-chicago-1";
private static final String COMPARTMENT_ID = System.getenv(OCI_COMPARTMENT_ID_KEY);
/**
* Create an OCIEmbeddingModel instance using a config file authentication provider.
* @return OCIEmbeddingModel instance
*/
public static OCIEmbeddingModel get() {
try {
ConfigFileAuthenticationDetailsProvider authProvider = new ConfigFileAuthenticationDetailsProvider(
CONFIG_FILE, PROFILE);
GenerativeAiInferenceClient aiClient = GenerativeAiInferenceClient.builder()
.region(Region.valueOf(REGION))
.build(authProvider);
OCIEmbeddingOptions options = OCIEmbeddingOptions.builder()
.withModel(EMBEDDING_MODEL_V2)
.withCompartment(COMPARTMENT_ID)
.withServingMode("on-demand")
.build();
return new OCIEmbeddingModel(aiClient, options);
}
catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2024 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.oci;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingRequest;
import org.springframework.ai.embedding.EmbeddingResponse;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.ai.oci.BaseEmbeddingModelTest.OCI_COMPARTMENT_ID_KEY;
@EnabledIfEnvironmentVariable(named = OCI_COMPARTMENT_ID_KEY, matches = ".+")
public class OCIEmbeddingModelIT extends BaseEmbeddingModelTest {
private final OCIEmbeddingModel embeddingModel = get();
private final List<String> content = List.of("How many states are in the USA?", "How many states are in India?");
@Test
void embed() {
float[] embedding = embeddingModel.embed(new Document("How many provinces are in Canada?"));
assertThat(embedding).hasSize(1024);
}
@Test
void call() {
EmbeddingResponse response = embeddingModel.call(new EmbeddingRequest(content, null));
assertThat(response).isNotNull();
assertThat(response.getResults()).hasSize(2);
assertThat(response.getMetadata().getModel()).isEqualTo(EMBEDDING_MODEL_V2);
}
@Test
void callWithOptions() {
EmbeddingResponse response = embeddingModel
.call(new EmbeddingRequest(content, OCIEmbeddingOptions.builder().withModel(EMBEDDING_MODEL_V3).build()));
assertThat(response).isNotNull();
assertThat(response.getResults()).hasSize(2);
assertThat(response.getMetadata().getModel()).isEqualTo(EMBEDDING_MODEL_V3);
}
}

View File

@@ -67,6 +67,7 @@
<module>models/spring-ai-huggingface</module>
<module>models/spring-ai-minimax</module>
<module>models/spring-ai-mistral-ai</module>
<module>models/spring-ai-oci-genai</module>
<module>models/spring-ai-ollama</module>
<module>models/spring-ai-openai</module>
<module>models/spring-ai-postgresml</module>
@@ -86,6 +87,7 @@
<module>spring-ai-spring-boot-starters/spring-ai-starter-huggingface</module>
<module>spring-ai-spring-boot-starters/spring-ai-starter-minimax</module>
<module>spring-ai-spring-boot-starters/spring-ai-starter-mistral-ai</module>
<module>spring-ai-spring-boot-starters/spring-ai-starter-oci-genai</module>
<module>spring-ai-spring-boot-starters/spring-ai-starter-ollama</module>
<module>spring-ai-spring-boot-starters/spring-ai-starter-openai</module>
<module>spring-ai-spring-boot-starters/spring-ai-starter-postgresml-embedding</module>
@@ -157,8 +159,10 @@
<awssdk.version>2.26.7</awssdk.version>
<jackson.version>2.16.1</jackson.version>
<djl.version>0.30.0</djl.version>
<onnxruntime.version>1.19.2</onnxruntime.version>
<oci-sdk-version>3.46.1</oci-sdk-version>
<com.google.cloud.version>26.41.0</com.google.cloud.version>
<qdrant.version>1.9.1</qdrant.version>
<spring-retry.version>2.0.5</spring-retry.version>

View File

@@ -76,6 +76,12 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-oci-genai</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama</artifactId>
@@ -313,6 +319,12 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-oci-genai-spring-boot-starter</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>

View File

@@ -34,7 +34,8 @@ public enum AiProvider {
OLLAMA("ollama"),
OPENAI("openai"),
SPRING_AI("spring_ai"),
VERTEX_AI("vertex_ai");
VERTEX_AI("vertex_ai"),
OCI_GENAI("oci_genai");
private final String value;

View File

@@ -37,13 +37,13 @@
// **** xref:api/chat/functions/zhipuai-chat-functions.adoc[Function Calling]
*** xref:api/chat/watsonx-ai-chat.adoc[watsonx.AI]
** xref:api/embeddings.adoc[]
*** xref:api/embeddings/azure-openai-embeddings.adoc[Azure OpenAI]
*** xref:api/bedrock.adoc[Amazon Bedrock]
**** xref:api/embeddings/bedrock-cohere-embedding.adoc[Cohere]
**** xref:api/embeddings/bedrock-titan-embedding.adoc[Titan]
*** xref:api/embeddings/azure-openai-embeddings.adoc[Azure OpenAI]
*** xref:api/embeddings/mistralai-embeddings.adoc[Mistral AI]
*** xref:api/embeddings/minimax-embeddings.adoc[MiniMax]
*** xref:api/embeddings/oci-genai-embeddings.adoc[OCI GenAI]
*** xref:api/embeddings/ollama-embeddings.adoc[Ollama]
*** xref:api/embeddings/onnx.adoc[(ONNX) Transformers]
*** xref:api/embeddings/openai-embeddings.adoc[OpenAI]

View File

@@ -168,3 +168,4 @@ Internally the various `EmbeddingModel` implementations use different low-level
* xref:api/embeddings/vertexai-embeddings-text.adoc[Spring AI VertexAI Embeddings]
* xref:api/embeddings/vertexai-embeddings-palm2.adoc[Spring AI VertexAI PaLM2 Embeddings]
* xref:api/embeddings/mistralai-embeddings.adoc[Spring AI Mistral AI Embeddings]
* xref:api/embeddings/oci-genai-embeddings.adoc[Spring AI Oracle Cloud Infrastructure GenAI Embeddings]

View File

@@ -0,0 +1,173 @@
= Oracle Cloud Infrastructure (OCI) GenAI Embeddings
https://www.oracle.com/artificial-intelligence/generative-ai/generative-ai-service/[OCI GenAI Service] offers text embedding with on-demand models, or dedicated AI clusters.
The https://docs.oracle.com/en-us/iaas/Content/generative-ai/embed-models.htm[OCI Embedding Models Page] and https://docs.oracle.com/en-us/iaas/Content/generative-ai/use-playground-embed.htm[OCI Text Embeddings Page] provide detailed information about using and hosting embedding models on OCI.
== Prerequisites
=== Add Repositories and BOM
Spring AI artifacts are published in Spring Milestone and Snapshot repositories. Refer to the xref:getting-started.adoc#repositories[Repositories] section to add these repositories to your build system.
To help with dependency management, Spring AI provides a BOM (bill of materials) to ensure that a consistent version of Spring AI is used throughout the entire project. Refer to the xref:getting-started.adoc#dependency-management[Dependency Management] section to add the Spring AI BOM to your build system.
== Auto-configuration
Spring AI provides Spring Boot auto-configuration for the OCI GenAI Embedding Client.
To enable it add the following dependency to your project's Maven `pom.xml` file:
[source, xml]
----
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-oci-genai-spring-boot-starter</artifactId>
</dependency>
----
or to your Gradle `build.gradle` build file.
[source,groovy]
----
dependencies {
implementation 'org.springframework.ai:spring-ai-oci-genai-spring-boot-starter'
}
----
TIP: Refer to the xref:getting-started.adoc#dependency-management[Dependency Management] section to add the Spring AI BOM to your build file.
=== Embedding Properties
The prefix `spring.ai.oci.genai` is the property prefix to configure the connection to OCI GenAI.
[cols="3,5,1"]
|====
| Property | Description | Default
| spring.ai.oci.genai.authenticationType | The type of authentication to use when authenticating to OCI. May be `file`, `instance-principal`, `workload-identity`, or `simple`. | file
| spring.ai.oci.genai.region | OCI service region. | us-chicago-1
| spring.ai.oci.genai.tenantId | OCI tenant OCID, used when authenticating with `simple` auth. | -
| spring.ai.oci.genai.userId | OCI user OCID, used when authenticating with `simple` auth. | -
| spring.ai.oci.genai.fingerprint | Private key fingerprint, used when authenticating with `simple` auth. | -
| spring.ai.oci.genai.privateKey | Private key content, used when authenticating with `simple` auth. | -
| spring.ai.oci.genai.passPhrase | Optional private key passphrase, used when authenticating with `simple` auth and a passphrase protected private key. | -
| spring.ai.oci.genai.file | Path to OCI config file. Used when authenticating with `file` auth. | <user's home directory>/.oci/config
| spring.ai.oci.genai.profile | OCI profile name. Used when authenticating with `file` auth. | DEFAULT
| spring.ai.oci.genai.endpoint | Optional OCI GenAI endpoint. | -
|====
The prefix `spring.ai.oci.genai.embedding` is the property prefix that configures the `EmbeddingModel` implementation for OCI GenAI
[cols="3,5,1"]
|====
| Property | Description | Default
| spring.ai.oci.genai.embedding.enabled | Enable OCI GenAI embedding model. | true
| spring.ai.oci.genai.embedding.compartment | Model compartment OCID. | -
| spring.ai.oci.genai.embedding.servingMode | The model serving mode to be used. May be `on-demand`, or `dedicated`. | on-demand
| spring.ai.oci.genai.embedding.truncate | How to truncate text if it overruns the embedding context. May be `START`, or `END`. | END
| spring.ai.oci.genai.embedding.model | The model or model endpoint used for embeddings. | -
|====
TIP: All properties prefixed with `spring.ai.oci.genai.embedding.options` can be overridden at runtime by adding a request specific <<embedding-options>> to the `EmbeddingRequest` call.
== Runtime Options [[embedding-options]]
The `OCIEmbeddingOptions` provides the configuration information for the embedding requests.
The `OCIEmbeddingOptions` offers a builder to create the options.
At start time use the `OCIEmbeddingOptions` constructor to set the default options used for all embedding requests.
At run-time you can override the default options, by passing a `OCIEmbeddingOptions` instance with your to the `EmbeddingRequest` request.
For example to override the default model name for a specific request:
[source,java]
----
EmbeddingResponse embeddingResponse = embeddingModel.call(
new EmbeddingRequest(List.of("Hello World", "World is big and salvation is near"),
OCIEmbeddingOptions.builder()
.withModel("my-other-embedding-model")
.build()
));
----
== Sample Code
This will create a `EmbeddingModel` implementation that you can inject into your class.
Here is an example of a simple `@Controller` class that uses the `EmbeddingModel` implementation.
[source,application.properties]
----
spring.ai.oci.genai.embedding.model=<your model>
spring.ai.oci.genai.embedding.compartment=<your model compartment>
----
[source,java]
----
@RestController
public class EmbeddingController {
private final EmbeddingModel embeddingModel;
@Autowired
public EmbeddingController(EmbeddingModel embeddingModel) {
this.embeddingModel = embeddingModel;
}
@GetMapping("/ai/embedding")
public Map embed(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
EmbeddingResponse embeddingResponse = this.embeddingModel.embedForResponse(List.of(message));
return Map.of("embedding", embeddingResponse);
}
}
----
== Manual Configuration
If you prefer not to use the Spring Boot auto-configuration, you can manually configure the `OCIEmbeddingModel` in your application.
For this add the `spring-oci-genai-openai` dependency to your project's Maven `pom.xml` file:
[source, xml]
----
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-oci-genai-openai</artifactId>
</dependency>
----
or to your Gradle `build.gradle` build file.
[source,gradle]
----
dependencies {
implementation 'org.springframework.ai:spring-oci-genai-openai'
}
----
TIP: Refer to the xref:getting-started.adoc#dependency-management[Dependency Management] section to add the Spring AI BOM to your build file.
Next, create an `OCIEmbeddingModel` instance and use it to compute the similarity between two input texts:
[source,java]
----
final String EMBEDDING_MODEL = "cohere.embed-english-light-v2.0";
final String CONFIG_FILE = Paths.get(System.getProperty("user.home"), ".oci", "config").toString();
final String PROFILE = "DEFAULT";
final String REGION = "us-chicago-1";
final String COMPARTMENT_ID = System.getenv("OCI_COMPARTMENT_ID");
var authProvider = new ConfigFileAuthenticationDetailsProvider(
CONFIG_FILE, PROFILE);
var aiClient = GenerativeAiInferenceClient.builder()
.region(Region.valueOf(REGION))
.build(authProvider);
var options = OCIEmbeddingOptions.builder()
.withModel(EMBEDDING_MODEL)
.withCompartment(COMPARTMENT_ID)
.withServingMode("on-demand")
.build();
var embeddingModel = new OCIEmbeddingModel(aiClient, options);
List<Double> embedding = embeddingModel.embed(new Document("How many provinces are in Canada?"));
----

View File

@@ -63,6 +63,13 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-oci-genai</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-huggingface</artifactId>
@@ -425,6 +432,13 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>oracle-free</artifactId>
<version>1.19.8</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>

View File

@@ -0,0 +1,150 @@
/*
* Copyright 2024 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.oci.genai;
import java.nio.file.Paths;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.StringUtils;
/**
* @author Anders Swanson
*/
@ConfigurationProperties(OCIConnectionProperties.CONFIG_PREFIX)
public class OCIConnectionProperties {
private static final String DEFAULT_PROFILE = "DEFAULT";
public static final String CONFIG_PREFIX = "spring.ai.oci.genai";
public enum AuthenticationType {
FILE("file"), INSTANCE_PRINCIPAL("instance-principal"), WORKLOAD_IDENTITY("workload-identity"),
SIMPLE("simple");
private final String authType;
AuthenticationType(String authType) {
this.authType = authType;
}
public String getAuthType() {
return this.authType;
}
}
private AuthenticationType authenticationType = AuthenticationType.FILE;
private String profile;
private String file = Paths.get(System.getProperty("user.home"), ".oci", "config").toString();
private String tenantId;
private String userId;
private String fingerprint;
private String privateKey;
private String passPhrase;
private String region = "us-chicago-1";
private String endpoint;
public String getRegion() {
return region;
}
public void setRegion(String region) {
this.region = region;
}
public String getPassPhrase() {
return passPhrase;
}
public void setPassPhrase(String passPhrase) {
this.passPhrase = passPhrase;
}
public String getPrivateKey() {
return privateKey;
}
public void setPrivateKey(String privateKey) {
this.privateKey = privateKey;
}
public String getFingerprint() {
return fingerprint;
}
public void setFingerprint(String fingerprint) {
this.fingerprint = fingerprint;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getTenantId() {
return tenantId;
}
public void setTenantId(String tenantId) {
this.tenantId = tenantId;
}
public String getFile() {
return file;
}
public void setFile(String file) {
this.file = file;
}
public String getProfile() {
return StringUtils.hasText(profile) ? profile : DEFAULT_PROFILE;
}
public void setProfile(String profile) {
this.profile = profile;
}
public AuthenticationType getAuthenticationType() {
return authenticationType;
}
public void setAuthenticationType(AuthenticationType authenticationType) {
this.authenticationType = authenticationType;
}
public String getEndpoint() {
return endpoint;
}
public void setEndpoint(String endpoint) {
this.endpoint = endpoint;
}
}

View File

@@ -0,0 +1,89 @@
/*
* Copyright 2024 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.oci.genai;
import com.oracle.bmc.generativeaiinference.model.EmbedTextDetails;
import org.springframework.ai.oci.OCIEmbeddingOptions;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author Anders Swanson
*/
@ConfigurationProperties(OCIEmbeddingModelProperties.CONFIG_PREFIX)
public class OCIEmbeddingModelProperties {
public static final String CONFIG_PREFIX = "spring.ai.oci.genai.embedding";
private ServingMode servingMode = ServingMode.ON_DEMAND;
private EmbedTextDetails.Truncate truncate = EmbedTextDetails.Truncate.End;
private String compartment;
private String model;
private boolean enabled;
public OCIEmbeddingOptions getEmbeddingOptions() {
return OCIEmbeddingOptions.builder()
.withCompartment(compartment)
.withModel(model)
.withServingMode(servingMode.getMode())
.withTruncate(truncate)
.build();
}
public ServingMode getServingMode() {
return servingMode;
}
public void setServingMode(ServingMode servingMode) {
this.servingMode = servingMode;
}
public String getCompartment() {
return compartment;
}
public void setCompartment(String compartment) {
this.compartment = compartment;
}
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public EmbedTextDetails.Truncate getTruncate() {
return truncate;
}
public void setTruncate(EmbedTextDetails.Truncate truncate) {
this.truncate = truncate;
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2024 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.oci.genai;
import java.io.IOException;
import com.oracle.bmc.ClientConfiguration;
import com.oracle.bmc.Region;
import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider;
import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider;
import com.oracle.bmc.auth.InstancePrincipalsAuthenticationDetailsProvider;
import com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider;
import com.oracle.bmc.auth.SimplePrivateKeySupplier;
import com.oracle.bmc.auth.okeworkloadidentity.OkeWorkloadIdentityAuthenticationDetailsProvider;
import com.oracle.bmc.generativeaiinference.GenerativeAiInferenceClient;
import com.oracle.bmc.retrier.RetryConfiguration;
import org.springframework.ai.oci.OCIEmbeddingModel;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.util.StringUtils;
/**
* @author Anders Swanson
*/
@AutoConfiguration
@ConditionalOnClass({ GenerativeAiInferenceClient.class, OCIEmbeddingModel.class })
@EnableConfigurationProperties({ OCIConnectionProperties.class, OCIEmbeddingModelProperties.class })
public class OCIGenAiAutoConfiguration {
@ConditionalOnMissingBean
@Bean
public GenerativeAiInferenceClient generativeAiInferenceClient(OCIConnectionProperties properties)
throws IOException {
ClientConfiguration clientConfiguration = ClientConfiguration.builder()
.retryConfiguration(RetryConfiguration.SDK_DEFAULT_RETRY_CONFIGURATION)
.build();
GenerativeAiInferenceClient.Builder builder = GenerativeAiInferenceClient.builder()
.configuration(clientConfiguration);
if (StringUtils.hasText(properties.getRegion())) {
builder.region(Region.valueOf(properties.getRegion()));
}
if (StringUtils.hasText(properties.getEndpoint())) {
builder.endpoint(properties.getEndpoint());
}
return builder.build(authenticationProvider(properties));
}
@Bean
@ConditionalOnProperty(prefix = OCIEmbeddingModelProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
matchIfMissing = true)
public OCIEmbeddingModel ociEmbeddingModel(GenerativeAiInferenceClient generativeAiClient,
OCIEmbeddingModelProperties properties) {
return new OCIEmbeddingModel(generativeAiClient, properties.getEmbeddingOptions());
}
private static BasicAuthenticationDetailsProvider authenticationProvider(OCIConnectionProperties properties)
throws IOException {
return switch (properties.getAuthenticationType()) {
case FILE -> new ConfigFileAuthenticationDetailsProvider(properties.getFile(), properties.getProfile());
case INSTANCE_PRINCIPAL -> InstancePrincipalsAuthenticationDetailsProvider.builder().build();
case WORKLOAD_IDENTITY -> OkeWorkloadIdentityAuthenticationDetailsProvider.builder().build();
case SIMPLE -> SimpleAuthenticationDetailsProvider.builder()
.userId(properties.getUserId())
.tenantId(properties.getTenantId())
.fingerprint(properties.getFingerprint())
.privateKeySupplier(new SimplePrivateKeySupplier(properties.getPrivateKey()))
.passPhrase(properties.getPassPhrase())
.region(Region.valueOf(properties.getRegion()))
.build();
};
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2024 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.oci.genai;
/**
* @author Anders Swanson
*/
public enum ServingMode {
ON_DEMAND("on-demand"), DEDICATED("dedicated");
private final String mode;
ServingMode(String mode) {
this.mode = mode;
}
public String getMode() {
return mode;
}
}

View File

@@ -1,5 +1,6 @@
org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration
org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiAutoConfiguration
org.springframework.ai.autoconfigure.oci.genai.OCIGenAiAutoConfiguration
org.springframework.ai.autoconfigure.stabilityai.StabilityAiImageAutoConfiguration
org.springframework.ai.autoconfigure.transformers.TransformersEmbeddingModelAutoConfiguration
org.springframework.ai.autoconfigure.huggingface.HuggingfaceChatAutoConfiguration

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2024 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.oci.genai;
import java.nio.file.Paths;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.springframework.ai.embedding.EmbeddingRequest;
import org.springframework.ai.embedding.EmbeddingResponse;
import org.springframework.ai.oci.OCIEmbeddingModel;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;
@EnabledIfEnvironmentVariable(named = OCIGenAiAutoConfigurationIT.COMPARTMENT_ID_KEY, matches = ".+")
public class OCIGenAiAutoConfigurationIT {
public static final String COMPARTMENT_ID_KEY = "OCI_COMPARTMENT_ID";
private final String CONFIG_FILE = Paths.get(System.getProperty("user.home"), ".oci", "config").toString();
private final String COMPARTMENT_ID = System.getenv(COMPARTMENT_ID_KEY);
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withPropertyValues(
// @formatter:off
"spring.ai.oci.genai.authenticationType=file",
"spring.ai.oci.genai.file=" + CONFIG_FILE,
"spring.ai.oci.genai.embedding.compartment=" + COMPARTMENT_ID,
"spring.ai.oci.genai.embedding.servingMode=on-demand",
"spring.ai.oci.genai.embedding.model=cohere.embed-english-light-v2.0"
// @formatter:on
).withConfiguration(AutoConfigurations.of(OCIGenAiAutoConfiguration.class));
@Test
void embeddings() {
contextRunner.run(context -> {
OCIEmbeddingModel embeddingModel = context.getBean(OCIEmbeddingModel.class);
assertThat(embeddingModel).isNotNull();
EmbeddingResponse response = embeddingModel
.call(new EmbeddingRequest(List.of("There are 50 states in the USA", "Canada has 10 provinces"), null));
assertThat(response).isNotNull();
assertThat(response.getResults()).hasSize(2);
});
}
}

View File

@@ -0,0 +1,40 @@
<?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>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>spring-ai-oci-genai-spring-boot-starter</artifactId>
<packaging>jar</packaging>
<name>Spring AI Starter - OCI GenAI</name>
<description>Spring AI OCI GenAI Auto Configuration</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.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-spring-boot-autoconfigure</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-oci-genai</artifactId>
<version>${project.parent.version}</version>
</dependency>
</dependencies>
</project>