Skip to content

Commit ad50f87

Browse files
committed
Provide documentation+release notes and add test for overlapping model setting
- test improvements
1 parent 30b276a commit ad50f87

File tree

4 files changed

+108
-34
lines changed

4 files changed

+108
-34
lines changed

docs/guides/SPRING_AI_INTEGRATION.md

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,12 @@ First, add the Spring AI dependency to your `pom.xml`:
3535
> [!NOTE]
3636
> Note that currently no stable version of Spring AI exists just yet.
3737
> The AI SDK currently uses the [M6 milestone](https://spring.io/blog/2025/02/14/spring-ai-1-0-0-m6-released).
38-
>
38+
>
3939
> Please be aware that future versions of the AI SDK may increase the Spring AI version.
4040
41-
## Orchestration Chat Completion
41+
## Orchestration
42+
43+
### Chat Completion
4244

4345
The Orchestration client is integrated in Spring AI classes:
4446

@@ -53,7 +55,7 @@ ChatResponse response = client.call(prompt);
5355

5456
Please find [an example in our Spring Boot application](../../sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/SpringAiOrchestrationService.java).
5557

56-
## Orchestration Masking
58+
### Masking
5759

5860
Configure Orchestration modules withing Spring AI:
5961

@@ -77,7 +79,7 @@ ChatResponse response = client.call(prompt);
7779
Please
7880
find [an example in our Spring Boot application](../../sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/SpringAiOrchestrationService.java).
7981

80-
## Stream chat completion
82+
### Stream chat completion
8183

8284
It's possible to pass a stream of chat completion delta elements, e.g. from the application backend
8385
to the frontend in real-time.
@@ -93,15 +95,15 @@ Prompt prompt =
9395
Flux<ChatResponse> flux = client.stream(prompt);
9496

9597
// also possible to keep only the chat completion text
96-
Flux<String> responseFlux =
98+
Flux<String> responseFlux =
9799
flux.map(chatResponse -> chatResponse.getResult().getOutput().getContent());
98100
```
99101

100102
_Note: A Spring endpoint can return `Flux` instead of `ResponseEntity`._
101103

102104
Please find [an example in our Spring Boot application](../../sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/SpringAiOrchestrationService.java).
103105

104-
## Tool Calling
106+
### Tool Calling
105107

106108
First define a function that will be called by the LLM:
107109

@@ -137,3 +139,24 @@ ChatResponse response = client.call(prompt);
137139

138140
Please find [an example in our Spring Boot application](../../sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/SpringAiOrchestrationService.java).
139141

142+
## OpenAI
143+
144+
### Embedding
145+
146+
With SpringAI integration (_since v1.5.0_), you may now obtain embedding vectors for a list of strings as follows:
147+
148+
You first initialize the OpenAI client for your model of choice and attach it `OpenAiSpringEmbeddingModel` object.
149+
150+
```java
151+
OpenAiClient client = OpenAiClient.forModel(OpenAiModel.TEXT_EMBEDDING_3_SMALL);
152+
OpenAiSpringEmbeddingModel embeddingModel = new OpenAiSpringEmbeddingModel(client);
153+
```
154+
155+
Then you can invoke `embded` method on the `embeddingModel` object with the text items to embed.
156+
157+
```java
158+
List<String> texts = List.of("Hello", "World");
159+
float[] embeddings = embeddingModel.embed(texts);
160+
```
161+
162+
Please find [an example in our Spring Boot application](../../sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/SpringAiOpenAiService.java).

docs/release-notes/release_notes.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
### ✨ New Functionality
1414

15-
-
15+
- [OpenAI] [Spring AI integration for embedding calls.](../guides/SPRING_AI_INTEGRATION.md#embedding)
1616

1717
### 📈 Improvements
1818

foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/spring/OpenAiSpringEmbeddingModel.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.sap.ai.sdk.foundationmodels.openai.spring;
22

3-
import com.google.common.annotations.Beta;
43
import com.sap.ai.sdk.foundationmodels.openai.OpenAiClient;
54
import com.sap.ai.sdk.foundationmodels.openai.generated.model.EmbeddingsCreate200Response;
65
import com.sap.ai.sdk.foundationmodels.openai.generated.model.EmbeddingsCreateRequest;
@@ -24,7 +23,6 @@
2423
*
2524
* @since 1.5.0
2625
*/
27-
@Beta
2826
public class OpenAiSpringEmbeddingModel implements EmbeddingModel {
2927

3028
private final OpenAiClient client;
@@ -39,9 +37,21 @@ public OpenAiSpringEmbeddingModel(@Nonnull final OpenAiClient client) {
3937
this.client = client;
4038
}
4139

40+
/**
41+
* {@inheritDoc}
42+
*
43+
* @throws IllegalArgumentException if {@code request.getOptions().getModel()} is not null.
44+
*/
4245
@Override
4346
@Nonnull
44-
public EmbeddingResponse call(@Nonnull final EmbeddingRequest request) {
47+
public EmbeddingResponse call(@Nonnull final EmbeddingRequest request)
48+
throws IllegalArgumentException {
49+
50+
if (request.getOptions().getModel() != null) {
51+
throw new IllegalArgumentException(
52+
"Invalid EmbeddingRequest: the model option must be null, as the client already defines the model.");
53+
}
54+
4555
final var openAiRequest = createEmbeddingCreateRequest(request);
4656
final var openAiResponse = client.embedding(openAiRequest);
4757

@@ -52,7 +62,7 @@ public EmbeddingResponse call(@Nonnull final EmbeddingRequest request) {
5262
@Nonnull
5363
public float[] embed(@Nonnull final Document document) throws UnsupportedOperationException {
5464
if (document.isText()) {
55-
return embed(Objects.requireNonNull(document.getText(), "Document text is null"));
65+
return embed(Objects.requireNonNull(document.getText(), "Document is missing text content"));
5666
}
5767
throw new UnsupportedOperationException("Only text type document supported for embedding");
5868
}

foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/spring/EmbeddingModelTest.java

Lines changed: 64 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22

33
import static org.assertj.core.api.Assertions.assertThat;
44
import static org.assertj.core.api.Assertions.assertThatThrownBy;
5-
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
6-
import static org.mockito.ArgumentMatchers.any;
5+
import static org.mockito.ArgumentMatchers.assertArg;
76
import static org.mockito.Mockito.mock;
87
import static org.mockito.Mockito.when;
98

@@ -12,8 +11,12 @@
1211
import com.sap.ai.sdk.foundationmodels.openai.generated.model.EmbeddingsCreate200ResponseDataInner;
1312
import com.sap.ai.sdk.foundationmodels.openai.generated.model.EmbeddingsCreate200ResponseUsage;
1413
import com.sap.ai.sdk.foundationmodels.openai.generated.model.EmbeddingsCreateRequest;
14+
import com.sap.ai.sdk.foundationmodels.openai.generated.model.EmbeddingsCreateRequestInput;
1515
import java.util.List;
16+
import java.util.function.Consumer;
17+
import lombok.val;
1618
import org.junit.jupiter.api.BeforeEach;
19+
import org.junit.jupiter.api.DisplayName;
1720
import org.junit.jupiter.api.Test;
1821
import org.springframework.ai.chat.metadata.DefaultUsage;
1922
import org.springframework.ai.document.Document;
@@ -32,55 +35,93 @@ void setUp() {
3235
}
3336

3437
@Test
35-
void callWithValidRequest() {
36-
final var request =
37-
new EmbeddingRequest(
38-
List.of("instructions"), EmbeddingOptionsBuilder.builder().withDimensions(128).build());
38+
@DisplayName("Call with embedding request containing valid options")
39+
void callWithValidEmbeddingRequest() {
40+
val texts = List.of("Some text");
41+
val springAiRequest =
42+
new EmbeddingRequest(texts, EmbeddingOptionsBuilder.builder().withDimensions(128).build());
43+
44+
val vector = new float[] {0.0f};
45+
val expectedOpenAiResponse =
46+
new EmbeddingsCreate200Response()
47+
.data(List.of(new EmbeddingsCreate200ResponseDataInner().embedding(vector)))
48+
.usage(new EmbeddingsCreate200ResponseUsage().promptTokens(0).totalTokens(0));
49+
50+
val expectedOpenAiRequest =
51+
new EmbeddingsCreateRequest()
52+
.input(EmbeddingsCreateRequestInput.create(texts))
53+
.dimensions(128);
3954

40-
final var vector = new float[] {0.0f};
41-
final var modelName = ""; // defined by client object and options not honoured
42-
final var expectedResponse =
55+
when(client.embedding(assertArg(assertRecursiveEquals(expectedOpenAiRequest))))
56+
.thenReturn(expectedOpenAiResponse);
57+
58+
val actualSpringAiResponse = new OpenAiSpringEmbeddingModel(client).call(springAiRequest);
59+
60+
val modelName = ""; // defined by client object and options not honoured
61+
val expectedSpringAiResponse =
4362
new EmbeddingResponse(
4463
List.of(new Embedding(vector, 0)),
4564
new EmbeddingResponseMetadata(modelName, new DefaultUsage(0, null, 0)));
4665

47-
final var openAiResponse =
48-
new EmbeddingsCreate200Response()
49-
.data(List.of(new EmbeddingsCreate200ResponseDataInner().embedding(vector)))
50-
.usage(new EmbeddingsCreate200ResponseUsage().promptTokens(0).totalTokens(0));
66+
assertThat(expectedSpringAiResponse)
67+
.usingRecursiveComparison()
68+
.isEqualTo(actualSpringAiResponse);
69+
}
5170

52-
when(client.embedding(any(EmbeddingsCreateRequest.class))).thenReturn(openAiResponse);
71+
@Test
72+
@DisplayName("Call with embedding request with model option set")
73+
void callWithModelOptionSetThrowsException() {
74+
val springAiRequest =
75+
new EmbeddingRequest(
76+
List.of("Some text"), EmbeddingOptionsBuilder.builder().withModel("model").build());
5377

54-
final var actualResponse = new OpenAiSpringEmbeddingModel(client).call(request);
78+
val model = new OpenAiSpringEmbeddingModel(client);
5579

56-
assertThat(expectedResponse).usingRecursiveComparison().isEqualTo(actualResponse);
80+
assertThatThrownBy(() -> model.call(springAiRequest))
81+
.isInstanceOf(IllegalArgumentException.class)
82+
.hasMessage(
83+
"Invalid EmbeddingRequest: the model option must be null, as the client already defines the model.");
5784
}
5885

5986
@Test
87+
@DisplayName("Embed document with text content")
6088
void embedDocument() {
6189
Document document = new Document("Some content");
6290

63-
var vector = new float[] {1, 2, 3};
64-
var openAiResponse =
91+
val vector = new float[] {1, 2, 3};
92+
val openAiResponse =
6593
new EmbeddingsCreate200Response()
6694
.data(List.of(new EmbeddingsCreate200ResponseDataInner().embedding(vector)))
6795
.usage(new EmbeddingsCreate200ResponseUsage().promptTokens(0).totalTokens(0));
6896

69-
when(client.embedding(any(EmbeddingsCreateRequest.class))).thenReturn(openAiResponse);
97+
val expectedOpenAiRequest =
98+
new EmbeddingsCreateRequest()
99+
.input(EmbeddingsCreateRequestInput.create(List.of(document.getText())));
100+
101+
when(client.embedding(assertArg(assertRecursiveEquals(expectedOpenAiRequest))))
102+
.thenReturn(openAiResponse);
70103

71104
float[] result = new OpenAiSpringEmbeddingModel(client).embed(document);
72-
assertArrayEquals(new float[] {1, 2, 3}, result);
105+
106+
assertThat(result).isEqualTo(new float[] {1, 2, 3});
73107
}
74108

75109
@Test
76-
void embedDocumentThrowsException() {
77-
var document = mock(Document.class);
110+
@DisplayName("Embed document with missing text content")
111+
void embedDocumentNonTextThrowsException() {
112+
val document = mock(Document.class);
78113
when(document.isText()).thenReturn(false);
79114

80-
var model = new OpenAiSpringEmbeddingModel(client);
115+
val model = new OpenAiSpringEmbeddingModel(client);
81116

82117
assertThatThrownBy(() -> model.embed(document))
83118
.isInstanceOf(UnsupportedOperationException.class)
84119
.hasMessage("Only text type document supported for embedding");
85120
}
121+
122+
private static <T> Consumer<T> assertRecursiveEquals(T expected) {
123+
return (actual) -> {
124+
assertThat(actual).usingRecursiveComparison().isEqualTo(expected);
125+
};
126+
}
86127
}

0 commit comments

Comments
 (0)