From ccf190c77cfa69db1051f318240636620b3e82e5 Mon Sep 17 00:00:00 2001 From: Anders Swanson Date: Tue, 20 Aug 2024 12:13:24 -0700 Subject: [PATCH] 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 --- models/spring-ai-oci-genai/README.md | 1 + models/spring-ai-oci-genai/pom.xml | 69 +++++++ .../ai/oci/OCIEmbeddingModel.java | 177 ++++++++++++++++++ .../ai/oci/OCIEmbeddingOptions.java | 114 +++++++++++ .../ai/oci/BaseEmbeddingModelTest.java | 64 +++++++ .../ai/oci/OCIEmbeddingModelIT.java | 59 ++++++ pom.xml | 4 + spring-ai-bom/pom.xml | 12 ++ .../observation/conventions/AiProvider.java | 3 +- .../src/main/antora/modules/ROOT/nav.adoc | 2 +- .../modules/ROOT/pages/api/embeddings.adoc | 1 + .../api/embeddings/oci-genai-embeddings.adoc | 173 +++++++++++++++++ spring-ai-spring-boot-autoconfigure/pom.xml | 14 ++ .../oci/genai/OCIConnectionProperties.java | 150 +++++++++++++++ .../genai/OCIEmbeddingModelProperties.java | 89 +++++++++ .../oci/genai/OCIGenAiAutoConfiguration.java | 90 +++++++++ .../autoconfigure/oci/genai/ServingMode.java | 35 ++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../genai/OCIGenAiAutoConfigurationIT.java | 62 ++++++ .../spring-ai-starter-oci-genai/pom.xml | 40 ++++ 20 files changed, 1158 insertions(+), 2 deletions(-) create mode 100644 models/spring-ai-oci-genai/README.md create mode 100644 models/spring-ai-oci-genai/pom.xml create mode 100644 models/spring-ai-oci-genai/src/main/java/org/springframework/ai/oci/OCIEmbeddingModel.java create mode 100644 models/spring-ai-oci-genai/src/main/java/org/springframework/ai/oci/OCIEmbeddingOptions.java create mode 100644 models/spring-ai-oci-genai/src/test/java/org/springframework/ai/oci/BaseEmbeddingModelTest.java create mode 100644 models/spring-ai-oci-genai/src/test/java/org/springframework/ai/oci/OCIEmbeddingModelIT.java create mode 100644 spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/oci-genai-embeddings.adoc create mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/oci/genai/OCIConnectionProperties.java create mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/oci/genai/OCIEmbeddingModelProperties.java create mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/oci/genai/OCIGenAiAutoConfiguration.java create mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/oci/genai/ServingMode.java create mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/oci/genai/OCIGenAiAutoConfigurationIT.java create mode 100644 spring-ai-spring-boot-starters/spring-ai-starter-oci-genai/pom.xml diff --git a/models/spring-ai-oci-genai/README.md b/models/spring-ai-oci-genai/README.md new file mode 100644 index 0000000000..16d73d5185 --- /dev/null +++ b/models/spring-ai-oci-genai/README.md @@ -0,0 +1 @@ +[Oracle Cloud Infrastructure GenAI Documentation](https://docs.oracle.com/en-us/iaas/Content/generative-ai/overview.htm) diff --git a/models/spring-ai-oci-genai/pom.xml b/models/spring-ai-oci-genai/pom.xml new file mode 100644 index 0000000000..fc007e9f9b --- /dev/null +++ b/models/spring-ai-oci-genai/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + + org.springframework.ai + spring-ai + 1.0.0-SNAPSHOT + ../../pom.xml + + spring-ai-oci-genai + jar + Spring AI Model - OCI GenAI + OCI GenAI models support + 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.ai + spring-ai-core + ${project.parent.version} + + + + com.oracle.oci.sdk + oci-java-sdk-shaded-full + ${oci-sdk-version} + + + + com.oracle.oci.sdk + oci-java-sdk-addons-oke-workload-identity + ${oci-sdk-version} + + + + + org.springframework.boot + spring-boot + + + + org.springframework + spring-context-support + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.ai + spring-ai-test + ${project.version} + test + + + + + diff --git a/models/spring-ai-oci-genai/src/main/java/org/springframework/ai/oci/OCIEmbeddingModel.java b/models/spring-ai-oci-genai/src/main/java/org/springframework/ai/oci/OCIEmbeddingModel.java new file mode 100644 index 0000000000..123e0705f3 --- /dev/null +++ b/models/spring-ai-oci-genai/src/main/java/org/springframework/ai/oci/OCIEmbeddingModel.java @@ -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 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 embedTextRequests, + EmbeddingModelObservationContext context) { + String modelId = null; + AtomicInteger index = new AtomicInteger(0); + List embeddings = new ArrayList<>(); + for (EmbedTextRequest embedTextRequest : embedTextRequests) { + EmbedTextResult embedTextResult = genAi.embedText(embedTextRequest).getEmbedTextResult(); + if (modelId == null) { + modelId = embedTextResult.getModelId(); + } + for (List 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 createRequests(List inputs, OCIEmbeddingOptions embeddingOptions) { + int size = inputs.size(); + List requests = new ArrayList<>(); + for (int i = 0; i < inputs.size(); i += EMBEDTEXT_BATCH_SIZE) { + List batch = inputs.subList(i, Math.min(i + EMBEDTEXT_BATCH_SIZE, size)); + requests.add(createRequest(batch, embeddingOptions)); + } + return requests; + } + + private EmbedTextRequest createRequest(List 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 embedding) { + float[] floats = new float[embedding.size()]; + for (int i = 0; i < embedding.size(); i++) { + floats[i] = embedding.get(i); + } + return floats; + } + +} diff --git a/models/spring-ai-oci-genai/src/main/java/org/springframework/ai/oci/OCIEmbeddingOptions.java b/models/spring-ai-oci-genai/src/main/java/org/springframework/ai/oci/OCIEmbeddingOptions.java new file mode 100644 index 0000000000..e72f5359f5 --- /dev/null +++ b/models/spring-ai-oci-genai/src/main/java/org/springframework/ai/oci/OCIEmbeddingOptions.java @@ -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; + } + +} diff --git a/models/spring-ai-oci-genai/src/test/java/org/springframework/ai/oci/BaseEmbeddingModelTest.java b/models/spring-ai-oci-genai/src/test/java/org/springframework/ai/oci/BaseEmbeddingModelTest.java new file mode 100644 index 0000000000..5124bd734e --- /dev/null +++ b/models/spring-ai-oci-genai/src/test/java/org/springframework/ai/oci/BaseEmbeddingModelTest.java @@ -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); + } + } + +} diff --git a/models/spring-ai-oci-genai/src/test/java/org/springframework/ai/oci/OCIEmbeddingModelIT.java b/models/spring-ai-oci-genai/src/test/java/org/springframework/ai/oci/OCIEmbeddingModelIT.java new file mode 100644 index 0000000000..8d25240bf8 --- /dev/null +++ b/models/spring-ai-oci-genai/src/test/java/org/springframework/ai/oci/OCIEmbeddingModelIT.java @@ -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 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); + } + +} diff --git a/pom.xml b/pom.xml index 3aa8ccff5d..cdbfe43f26 100644 --- a/pom.xml +++ b/pom.xml @@ -67,6 +67,7 @@ models/spring-ai-huggingface models/spring-ai-minimax models/spring-ai-mistral-ai + models/spring-ai-oci-genai models/spring-ai-ollama models/spring-ai-openai models/spring-ai-postgresml @@ -86,6 +87,7 @@ spring-ai-spring-boot-starters/spring-ai-starter-huggingface spring-ai-spring-boot-starters/spring-ai-starter-minimax spring-ai-spring-boot-starters/spring-ai-starter-mistral-ai + spring-ai-spring-boot-starters/spring-ai-starter-oci-genai spring-ai-spring-boot-starters/spring-ai-starter-ollama spring-ai-spring-boot-starters/spring-ai-starter-openai spring-ai-spring-boot-starters/spring-ai-starter-postgresml-embedding @@ -157,8 +159,10 @@ 2.26.7 2.16.1 + 0.30.0 1.19.2 + 3.46.1 26.41.0 1.9.1 2.0.5 diff --git a/spring-ai-bom/pom.xml b/spring-ai-bom/pom.xml index 805d8fbbb7..838fabcce3 100644 --- a/spring-ai-bom/pom.xml +++ b/spring-ai-bom/pom.xml @@ -76,6 +76,12 @@ ${project.version} + + org.springframework.ai + spring-ai-oci-genai + ${project.version} + + org.springframework.ai spring-ai-ollama @@ -313,6 +319,12 @@ ${project.version} + + org.springframework.ai + spring-ai-oci-genai-spring-boot-starter + ${project.version} + + org.springframework.ai spring-ai-ollama-spring-boot-starter diff --git a/spring-ai-core/src/main/java/org/springframework/ai/observation/conventions/AiProvider.java b/spring-ai-core/src/main/java/org/springframework/ai/observation/conventions/AiProvider.java index e3da38c628..cb9a76aea1 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/observation/conventions/AiProvider.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/observation/conventions/AiProvider.java @@ -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; diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc index 7c544adf65..873cce6f44 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc @@ -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] diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings.adoc index f796b9a6f9..f99c5442c6 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings.adoc @@ -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] diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/oci-genai-embeddings.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/oci-genai-embeddings.adoc new file mode 100644 index 0000000000..2912d39350 --- /dev/null +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/oci-genai-embeddings.adoc @@ -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] +---- + + org.springframework.ai + spring-ai-oci-genai-spring-boot-starter + +---- + +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. | /.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 <> 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= +spring.ai.oci.genai.embedding.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] +---- + + org.springframework.ai + spring-oci-genai-openai + +---- + +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 embedding = embeddingModel.embed(new Document("How many provinces are in Canada?")); +---- diff --git a/spring-ai-spring-boot-autoconfigure/pom.xml b/spring-ai-spring-boot-autoconfigure/pom.xml index 250d6246ca..9a0428fea0 100644 --- a/spring-ai-spring-boot-autoconfigure/pom.xml +++ b/spring-ai-spring-boot-autoconfigure/pom.xml @@ -63,6 +63,13 @@ true + + org.springframework.ai + spring-ai-oci-genai + ${project.parent.version} + true + + org.springframework.ai spring-ai-huggingface @@ -425,6 +432,13 @@ test + + org.testcontainers + oracle-free + 1.19.8 + test + + org.testcontainers junit-jupiter diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/oci/genai/OCIConnectionProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/oci/genai/OCIConnectionProperties.java new file mode 100644 index 0000000000..6de993705d --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/oci/genai/OCIConnectionProperties.java @@ -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; + } + +} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/oci/genai/OCIEmbeddingModelProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/oci/genai/OCIEmbeddingModelProperties.java new file mode 100644 index 0000000000..cc2ef2b5f1 --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/oci/genai/OCIEmbeddingModelProperties.java @@ -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; + } + +} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/oci/genai/OCIGenAiAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/oci/genai/OCIGenAiAutoConfiguration.java new file mode 100644 index 0000000000..9e8a64059f --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/oci/genai/OCIGenAiAutoConfiguration.java @@ -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(); + }; + } + +} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/oci/genai/ServingMode.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/oci/genai/ServingMode.java new file mode 100644 index 0000000000..7cb2299a2c --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/oci/genai/ServingMode.java @@ -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; + } + +} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 9fe46cc260..d5b849aa3c 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,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 diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/oci/genai/OCIGenAiAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/oci/genai/OCIGenAiAutoConfigurationIT.java new file mode 100644 index 0000000000..d67a8e0e71 --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/oci/genai/OCIGenAiAutoConfigurationIT.java @@ -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); + }); + } + +} diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-oci-genai/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-oci-genai/pom.xml new file mode 100644 index 0000000000..2fd347de32 --- /dev/null +++ b/spring-ai-spring-boot-starters/spring-ai-starter-oci-genai/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + org.springframework.ai + spring-ai + 1.0.0-SNAPSHOT + ../../pom.xml + + spring-ai-oci-genai-spring-boot-starter + jar + Spring AI Starter - OCI GenAI + Spring AI OCI GenAI Auto Configuration + 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.boot + spring-boot-starter + + + + org.springframework.ai + spring-ai-spring-boot-autoconfigure + ${project.parent.version} + + + + org.springframework.ai + spring-ai-oci-genai + ${project.parent.version} + + +