From 040e7a85a2a449baf77e93208f15a0c2c1cd52ef Mon Sep 17 00:00:00 2001 From: fzowl Date: Tue, 8 Sep 2026 18:48:16 +0200 Subject: [PATCH] Rebrand VoyageAI as 'VoyageAI by MongoDB', refresh model list, support Union inputs for contextualized embeddings - Rebrand user-facing VoyageAI references to 'VoyageAI by MongoDB' in PACKAGES.md and connector Javadoc. - Update documented model lists to currently available models per docs.voyageai.com (voyage-4 family, voyage-context-4, voyage-multimodal-3.5, rerank-2.5); bump integration test defaults accordingly. - Support the official contextualized embeddings inputs: Union[List[List[str]], List[str]] spec by adding a flat-document overload (generateContextualizedEmbeddingsForDocumentsAsync) with server-side auto-chunking, alongside the existing nested form; add unit coverage. - Tolerate unknown fields on embedding/rerank response items to match current API responses. --- PACKAGES.md | 6 +- ...textualizedEmbeddingGenerationService.java | 52 ++++++++++++++-- .../voyageai/core/VoyageAIClient.java | 2 +- .../voyageai/core/VoyageAIModels.java | 43 ++++++++++++-- ...IMultimodalEmbeddingGenerationService.java | 8 +-- .../VoyageAITextRerankingService.java | 9 +-- ...oyageAITextEmbeddingGenerationService.java | 10 ++-- ...ualizedEmbeddingGenerationServiceTest.java | 59 ++++++++++++++++--- .../voyageai/VoyageAIIntegrationTest.java | 8 +-- 9 files changed, 160 insertions(+), 37 deletions(-) diff --git a/PACKAGES.md b/PACKAGES.md index eab2cdf0..4a9d4704 100644 --- a/PACKAGES.md +++ b/PACKAGES.md @@ -40,7 +40,7 @@ A BOM is provided that can be used to define the versions of all Semantic Kernel : Provides a connector that can be used to interact with the OpenAI API. `semantickernel-aiservices-voyageai` -: Provides connectors for VoyageAI's embedding and reranking services, including text embeddings, contextualized embeddings, multimodal embeddings, and document reranking. +: Provides connectors for VoyageAI by MongoDB's embedding and reranking services, including text embeddings (e.g. voyage-4-large, voyage-4, voyage-code-4), contextualized embeddings (voyage-context-4), multimodal embeddings (voyage-multimodal-3.5), and document reranking (rerank-2.5). ## Example Configurations @@ -75,9 +75,9 @@ POM XML for a simple project that uses OpenAI. ``` -### Example: VoyageAI Embeddings and Reranking +### Example: VoyageAI by MongoDB Embeddings and Reranking -POM XML for a project that uses VoyageAI for embeddings and reranking. +POM XML for a project that uses VoyageAI by MongoDB for embeddings and reranking. ```xml diff --git a/aiservices/voyageai/src/main/java/com/microsoft/semantickernel/aiservices/voyageai/contextualizedembedding/VoyageAIContextualizedEmbeddingGenerationService.java b/aiservices/voyageai/src/main/java/com/microsoft/semantickernel/aiservices/voyageai/contextualizedembedding/VoyageAIContextualizedEmbeddingGenerationService.java index 2ccb3990..edcf0de5 100644 --- a/aiservices/voyageai/src/main/java/com/microsoft/semantickernel/aiservices/voyageai/contextualizedembedding/VoyageAIContextualizedEmbeddingGenerationService.java +++ b/aiservices/voyageai/src/main/java/com/microsoft/semantickernel/aiservices/voyageai/contextualizedembedding/VoyageAIContextualizedEmbeddingGenerationService.java @@ -19,9 +19,9 @@ import java.util.stream.Collectors; /** - * VoyageAI contextualized embedding generation service. + * VoyageAI by MongoDB contextualized embedding generation service. * Generates embeddings that capture both local chunk details and global document-level metadata. - * Supports models like voyage-3. + * Supports voyage-context-4 (current) and voyage-context-3. */ public final class VoyageAIContextualizedEmbeddingGenerationService implements TextEmbeddingGenerationService { @@ -35,7 +35,7 @@ public final class VoyageAIContextualizedEmbeddingGenerationService implements T * Creates a new instance of VoyageAI contextualized embedding generation service. * * @param client VoyageAI client - * @param modelId Model ID (e.g., "voyage-3") + * @param modelId Model ID (e.g., "voyage-context-4") * @param serviceId Optional service ID */ public VoyageAIContextualizedEmbeddingGenerationService( @@ -66,9 +66,14 @@ public String getModelId() { } /** - * Generates contextualized embeddings for document chunks. + * Generates contextualized embeddings for pre-chunked documents. * - * @param inputs List of lists where each inner list contains document chunks + *

Sends the inputs in the nested form of the official + * {@code inputs: Union[List[List[str]], List[str]]} specification, where each inner + * list holds one document's chunks and is embedded as a group so every chunk is + * encoded in the context of the others. + * + * @param inputs List of lists where each inner list contains one document's chunks * @return A Mono containing a list of embeddings for all chunks across all documents */ public Mono> generateContextualizedEmbeddingsAsync(List> inputs) { @@ -84,6 +89,41 @@ public Mono> generateContextualizedEmbeddingsAsync(ListSends the inputs in the flat form of the official + * {@code inputs: Union[List[List[str]], List[str]]} specification and enables + * server-side auto-chunking, so each document is split and every resulting chunk is + * embedded with document-level context. Per the API contract the flat, document-typed + * form requires auto-chunking, so {@code input_type} is set to {@code document} and + * {@code enable_auto_chunking} to {@code true}. + * + * @param documents flat list of full-document strings + * @return A Mono containing a list of embeddings for all chunks across all documents + */ + public Mono> generateContextualizedEmbeddingsForDocumentsAsync(List documents) { + if (documents == null || documents.isEmpty()) { + return Mono.just(Collections.emptyList()); + } + + LOGGER.debug("Generating contextualized embeddings for {} documents (auto-chunking) using model {}", + documents.size(), modelId); + + VoyageAIModels.ContextualizedEmbeddingRequest request = + new VoyageAIModels.ContextualizedEmbeddingRequest(); + request.setFlatInputs(documents); + request.setModel(modelId); + request.setInputType("document"); + request.setEnableAutoChunking(true); + + return sendRequest(request); + } + + private Mono> sendRequest(VoyageAIModels.ContextualizedEmbeddingRequest request) { return client.sendRequestAsync( "contextualizedembeddings", request, @@ -173,7 +213,7 @@ public Builder withClient(VoyageAIClient client) { /** * Sets the model ID. * - * @param modelId Model ID (e.g., "voyage-3") + * @param modelId Model ID (e.g., "voyage-context-4") * @return This builder */ public Builder withModelId(String modelId) { diff --git a/aiservices/voyageai/src/main/java/com/microsoft/semantickernel/aiservices/voyageai/core/VoyageAIClient.java b/aiservices/voyageai/src/main/java/com/microsoft/semantickernel/aiservices/voyageai/core/VoyageAIClient.java index 856a3397..b8f24657 100644 --- a/aiservices/voyageai/src/main/java/com/microsoft/semantickernel/aiservices/voyageai/core/VoyageAIClient.java +++ b/aiservices/voyageai/src/main/java/com/microsoft/semantickernel/aiservices/voyageai/core/VoyageAIClient.java @@ -18,7 +18,7 @@ import java.util.concurrent.TimeUnit; /** - * HTTP client for VoyageAI API. + * HTTP client for the VoyageAI by MongoDB API. */ public final class VoyageAIClient { private static final Logger LOGGER = LoggerFactory.getLogger(VoyageAIClient.class); diff --git a/aiservices/voyageai/src/main/java/com/microsoft/semantickernel/aiservices/voyageai/core/VoyageAIModels.java b/aiservices/voyageai/src/main/java/com/microsoft/semantickernel/aiservices/voyageai/core/VoyageAIModels.java index c1e85387..a813a8d7 100644 --- a/aiservices/voyageai/src/main/java/com/microsoft/semantickernel/aiservices/voyageai/core/VoyageAIModels.java +++ b/aiservices/voyageai/src/main/java/com/microsoft/semantickernel/aiservices/voyageai/core/VoyageAIModels.java @@ -8,7 +8,7 @@ import java.util.List; /** - * VoyageAI API request and response models. + * VoyageAI by MongoDB API request and response models. */ public class VoyageAIModels { @@ -122,6 +122,7 @@ public void setUsage(EmbeddingUsage usage) { /** * Embedding data item. */ + @JsonIgnoreProperties(ignoreUnknown = true) public static class EmbeddingDataItem { @JsonProperty("object") private String object; @@ -275,6 +276,7 @@ public void setUsage(EmbeddingUsage usage) { /** * Rerank data item. */ + @JsonIgnoreProperties(ignoreUnknown = true) public static class RerankDataItem { @JsonProperty("index") private int index; @@ -303,11 +305,18 @@ public void setRelevanceScore(double relevanceScore) { /** * Request model for contextualized embeddings. + * + *

The VoyageAI API accepts {@code inputs} as either a nested list of chunks + * ({@code List>}, one inner list per document) or a flat list of + * full-document strings ({@code List}), matching the official + * {@code inputs: Union[List[List[str]], List[str]]} specification. The flat form + * is used together with server-side auto-chunking. */ @JsonInclude(JsonInclude.Include.NON_NULL) public static class ContextualizedEmbeddingRequest { + // Holds either List> (nested chunks) or List (flat documents). @JsonProperty("inputs") - private List> inputs; + private Object inputs; @JsonProperty("model") private String model; @@ -324,16 +333,41 @@ public static class ContextualizedEmbeddingRequest { @JsonProperty("output_dtype") private String outputDtype; + @JsonProperty("enable_auto_chunking") + private Boolean enableAutoChunking; + @SuppressFBWarnings("EI_EXPOSE_REP") - public List> getInputs() { + public Object getInputs() { return inputs; } - @SuppressFBWarnings("EI_EXPOSE_REP2") + /** + * Sets the inputs as a nested list of chunks, one inner list per document. + * + * @param inputs nested list of document chunks + */ public void setInputs(List> inputs) { this.inputs = inputs; } + /** + * Sets the inputs as a flat list of full-document strings. Use together with + * server-side auto-chunking (see {@link #setEnableAutoChunking(Boolean)}). + * + * @param inputs flat list of full-document strings + */ + public void setFlatInputs(List inputs) { + this.inputs = inputs; + } + + public Boolean getEnableAutoChunking() { + return enableAutoChunking; + } + + public void setEnableAutoChunking(Boolean enableAutoChunking) { + this.enableAutoChunking = enableAutoChunking; + } + public String getModel() { return model; } @@ -435,6 +469,7 @@ public void setEmbeddings(List embeddings) { /** * Embedding item with chunk information. */ + @JsonIgnoreProperties(ignoreUnknown = true) public static class EmbeddingItem { @JsonProperty("embedding") private float[] embedding; diff --git a/aiservices/voyageai/src/main/java/com/microsoft/semantickernel/aiservices/voyageai/multimodalembedding/VoyageAIMultimodalEmbeddingGenerationService.java b/aiservices/voyageai/src/main/java/com/microsoft/semantickernel/aiservices/voyageai/multimodalembedding/VoyageAIMultimodalEmbeddingGenerationService.java index 89304d37..42b0755c 100644 --- a/aiservices/voyageai/src/main/java/com/microsoft/semantickernel/aiservices/voyageai/multimodalembedding/VoyageAIMultimodalEmbeddingGenerationService.java +++ b/aiservices/voyageai/src/main/java/com/microsoft/semantickernel/aiservices/voyageai/multimodalembedding/VoyageAIMultimodalEmbeddingGenerationService.java @@ -19,9 +19,9 @@ import java.util.stream.Collectors; /** - * VoyageAI multimodal embedding generation service. + * VoyageAI by MongoDB multimodal embedding generation service. * Generates embeddings for text, images, or interleaved text and images. - * Supports the voyage-multimodal-3 model. + * Supports voyage-multimodal-3.5 (current) and voyage-multimodal-3. *

* Constraints: * - Maximum 1,000 inputs per request @@ -41,7 +41,7 @@ public final class VoyageAIMultimodalEmbeddingGenerationService implements TextE * Creates a new instance of VoyageAI multimodal embedding generation service. * * @param client VoyageAI client - * @param modelId Model ID (e.g., "voyage-multimodal-3") + * @param modelId Model ID (e.g., "voyage-multimodal-3.5") * @param serviceId Optional service ID */ public VoyageAIMultimodalEmbeddingGenerationService( @@ -196,7 +196,7 @@ public Builder withClient(VoyageAIClient client) { /** * Sets the model ID. * - * @param modelId Model ID (e.g., "voyage-multimodal-3") + * @param modelId Model ID (e.g., "voyage-multimodal-3.5") * @return This builder */ public Builder withModelId(String modelId) { diff --git a/aiservices/voyageai/src/main/java/com/microsoft/semantickernel/aiservices/voyageai/reranking/VoyageAITextRerankingService.java b/aiservices/voyageai/src/main/java/com/microsoft/semantickernel/aiservices/voyageai/reranking/VoyageAITextRerankingService.java index cda093d7..5616ae76 100644 --- a/aiservices/voyageai/src/main/java/com/microsoft/semantickernel/aiservices/voyageai/reranking/VoyageAITextRerankingService.java +++ b/aiservices/voyageai/src/main/java/com/microsoft/semantickernel/aiservices/voyageai/reranking/VoyageAITextRerankingService.java @@ -17,8 +17,9 @@ import java.util.stream.Collectors; /** - * VoyageAI implementation of {@link TextRerankingService}. - * Supports models like rerank-2, rerank-2-lite. + * VoyageAI by MongoDB implementation of {@link TextRerankingService}. + * Supports current models such as rerank-2.5 and rerank-2.5-lite (and preview models + * rerank-3, rerank-3-lite), as well as older models like rerank-2 and rerank-2-lite. */ public final class VoyageAITextRerankingService implements TextRerankingService { @@ -33,7 +34,7 @@ public final class VoyageAITextRerankingService implements TextRerankingService * Creates a new instance of VoyageAI text reranking service. * * @param client VoyageAI client - * @param modelId Model ID (e.g., "rerank-2") + * @param modelId Model ID (e.g., "rerank-2.5") * @param serviceId Optional service ID * @param topK Optional top K results to return */ @@ -140,7 +141,7 @@ public Builder withClient(VoyageAIClient client) { /** * Sets the model ID. * - * @param modelId Model ID (e.g., "rerank-2") + * @param modelId Model ID (e.g., "rerank-2.5") * @return This builder */ public Builder withModelId(String modelId) { diff --git a/aiservices/voyageai/src/main/java/com/microsoft/semantickernel/aiservices/voyageai/textembedding/VoyageAITextEmbeddingGenerationService.java b/aiservices/voyageai/src/main/java/com/microsoft/semantickernel/aiservices/voyageai/textembedding/VoyageAITextEmbeddingGenerationService.java index 4345aca5..e3c208ad 100644 --- a/aiservices/voyageai/src/main/java/com/microsoft/semantickernel/aiservices/voyageai/textembedding/VoyageAITextEmbeddingGenerationService.java +++ b/aiservices/voyageai/src/main/java/com/microsoft/semantickernel/aiservices/voyageai/textembedding/VoyageAITextEmbeddingGenerationService.java @@ -18,8 +18,10 @@ import java.util.stream.Collectors; /** - * VoyageAI implementation of {@link TextEmbeddingGenerationService}. - * Supports models like voyage-3-large, voyage-3.5, voyage-code-3, voyage-finance-2, voyage-law-2. + * VoyageAI by MongoDB implementation of {@link TextEmbeddingGenerationService}. + * Supports current models such as voyage-4-large, voyage-4, voyage-4-lite, voyage-code-4, + * voyage-finance-2 and voyage-law-2, as well as older models like voyage-3-large, + * voyage-3.5, voyage-3.5-lite and voyage-code-3. */ public final class VoyageAITextEmbeddingGenerationService implements TextEmbeddingGenerationService { @@ -33,7 +35,7 @@ public final class VoyageAITextEmbeddingGenerationService implements TextEmbeddi * Creates a new instance of VoyageAI text embedding generation service. * * @param client VoyageAI client - * @param modelId Model ID (e.g., "voyage-3-large") + * @param modelId Model ID (e.g., "voyage-4-large") * @param serviceId Optional service ID */ public VoyageAITextEmbeddingGenerationService( @@ -143,7 +145,7 @@ public Builder withClient(VoyageAIClient client) { /** * Sets the model ID. * - * @param modelId Model ID (e.g., "voyage-3-large") + * @param modelId Model ID (e.g., "voyage-4-large") * @return This builder */ public Builder withModelId(String modelId) { diff --git a/aiservices/voyageai/src/test/java/com/microsoft/semantickernel/aiservices/voyageai/VoyageAIContextualizedEmbeddingGenerationServiceTest.java b/aiservices/voyageai/src/test/java/com/microsoft/semantickernel/aiservices/voyageai/VoyageAIContextualizedEmbeddingGenerationServiceTest.java index 8b4d2260..80dff7c0 100644 --- a/aiservices/voyageai/src/test/java/com/microsoft/semantickernel/aiservices/voyageai/VoyageAIContextualizedEmbeddingGenerationServiceTest.java +++ b/aiservices/voyageai/src/test/java/com/microsoft/semantickernel/aiservices/voyageai/VoyageAIContextualizedEmbeddingGenerationServiceTest.java @@ -6,6 +6,7 @@ import com.microsoft.semantickernel.aiservices.voyageai.core.VoyageAIModels; import com.microsoft.semantickernel.services.textembedding.Embedding; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import reactor.core.publisher.Mono; @@ -50,7 +51,7 @@ public void testGenerateContextualizedEmbeddings() { .thenReturn(Mono.just(mockResponse)); VoyageAIContextualizedEmbeddingGenerationService service = - new VoyageAIContextualizedEmbeddingGenerationService(mockClient, "voyage-3", null); + new VoyageAIContextualizedEmbeddingGenerationService(mockClient, "voyage-context-4", null); List> inputs = Arrays.asList( Arrays.asList("chunk1"), @@ -67,6 +68,50 @@ public void testGenerateContextualizedEmbeddings() { assertEquals(expected2, results.get(1).getVector()); } + @Test + public void testGenerateContextualizedEmbeddingsForDocumentsSendsFlatInputs() { + VoyageAIClient mockClient = Mockito.mock(VoyageAIClient.class); + + VoyageAIModels.ContextualizedEmbeddingResponse mockResponse = + new VoyageAIModels.ContextualizedEmbeddingResponse(); + + VoyageAIModels.EmbeddingDataItem item = new VoyageAIModels.EmbeddingDataItem(); + item.setEmbedding(new float[]{0.5f, 0.6f}); + item.setIndex(0); + + VoyageAIModels.ContextualizedEmbeddingDataList dataList = + new VoyageAIModels.ContextualizedEmbeddingDataList(); + dataList.setData(Arrays.asList(item)); + mockResponse.setData(Arrays.asList(dataList)); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(Object.class); + when(mockClient.sendRequestAsync( + eq("contextualizedembeddings"), + requestCaptor.capture(), + eq(VoyageAIModels.ContextualizedEmbeddingResponse.class))) + .thenReturn(Mono.just(mockResponse)); + + VoyageAIContextualizedEmbeddingGenerationService service = + new VoyageAIContextualizedEmbeddingGenerationService(mockClient, "voyage-context-4", null); + + // Flat form of inputs: Union[List[List[str]], List[str]] -> List[str] + List documents = Arrays.asList("full document one", "full document two"); + + List results = + service.generateContextualizedEmbeddingsForDocumentsAsync(documents).block(); + + assertNotNull(results); + assertEquals(1, results.size()); + + VoyageAIModels.ContextualizedEmbeddingRequest sent = + (VoyageAIModels.ContextualizedEmbeddingRequest) requestCaptor.getValue(); + // inputs must be serialized as a flat List, not a nested list + assertTrue(sent.getInputs() instanceof List); + assertEquals(documents, sent.getInputs()); + assertEquals("document", sent.getInputType()); + assertEquals(Boolean.TRUE, sent.getEnableAutoChunking()); + } + @Test public void testGenerateEmbedding() { VoyageAIClient mockClient = Mockito.mock(VoyageAIClient.class); @@ -91,7 +136,7 @@ public void testGenerateEmbedding() { .thenReturn(Mono.just(mockResponse)); VoyageAIContextualizedEmbeddingGenerationService service = - new VoyageAIContextualizedEmbeddingGenerationService(mockClient, "voyage-3", null); + new VoyageAIContextualizedEmbeddingGenerationService(mockClient, "voyage-context-4", null); Embedding result2 = service.generateEmbeddingAsync("test text").block(); @@ -105,10 +150,10 @@ public void testServiceIdAndModelId() { VoyageAIClient mockClient = Mockito.mock(VoyageAIClient.class); VoyageAIContextualizedEmbeddingGenerationService service = - new VoyageAIContextualizedEmbeddingGenerationService(mockClient, "voyage-3", "test-service"); + new VoyageAIContextualizedEmbeddingGenerationService(mockClient, "voyage-context-4", "test-service"); assertEquals("test-service", service.getServiceId()); - assertEquals("voyage-3", service.getModelId()); + assertEquals("voyage-context-4", service.getModelId()); } @Test @@ -118,19 +163,19 @@ public void testBuilderPattern() { VoyageAIContextualizedEmbeddingGenerationService service = VoyageAIContextualizedEmbeddingGenerationService.builder() .withClient(mockClient) - .withModelId("voyage-3") + .withModelId("voyage-context-4") .withServiceId("test-service") .build(); assertNotNull(service); assertEquals("test-service", service.getServiceId()); - assertEquals("voyage-3", service.getModelId()); + assertEquals("voyage-context-4", service.getModelId()); } @Test public void testNullClientThrowsException() { assertThrows(IllegalArgumentException.class, () -> - new VoyageAIContextualizedEmbeddingGenerationService(null, "voyage-3", null)); + new VoyageAIContextualizedEmbeddingGenerationService(null, "voyage-context-4", null)); } @Test diff --git a/aiservices/voyageai/src/test/java/com/microsoft/semantickernel/aiservices/voyageai/VoyageAIIntegrationTest.java b/aiservices/voyageai/src/test/java/com/microsoft/semantickernel/aiservices/voyageai/VoyageAIIntegrationTest.java index b9631d64..6ffc3321 100644 --- a/aiservices/voyageai/src/test/java/com/microsoft/semantickernel/aiservices/voyageai/VoyageAIIntegrationTest.java +++ b/aiservices/voyageai/src/test/java/com/microsoft/semantickernel/aiservices/voyageai/VoyageAIIntegrationTest.java @@ -25,10 +25,10 @@ public class VoyageAIIntegrationTest { private static final String API_KEY_ENV_VAR = "VOYAGE_API_KEY"; - private static final String DEFAULT_EMBEDDING_MODEL = "voyage-3-large"; - private static final String DEFAULT_CONTEXTUALIZED_MODEL = "voyage-context-3"; - private static final String DEFAULT_MULTIMODAL_MODEL = "voyage-multimodal-3"; - private static final String DEFAULT_RERANK_MODEL = "rerank-2"; + private static final String DEFAULT_EMBEDDING_MODEL = "voyage-4-large"; + private static final String DEFAULT_CONTEXTUALIZED_MODEL = "voyage-context-4"; + private static final String DEFAULT_MULTIMODAL_MODEL = "voyage-multimodal-3.5"; + private static final String DEFAULT_RERANK_MODEL = "rerank-2.5"; private String apiKey;