Skip to content

Commit 8df9ef8

Browse files
authored
Merge branch 'main' into generated-openai
2 parents 753cbc4 + 97bc3b5 commit 8df9ef8

File tree

11 files changed

+274
-61
lines changed

11 files changed

+274
-61
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ The following table lists the required versions, based on the latest release:
4747

4848
See [an example `pom.xml` in our Spring Boot application](sample-code/spring-app/pom.xml).
4949

50+
> [!WARNING]
51+
> All classes under any of the `...model` packages are generated from an OpenAPI specification and marked as `@Beta`.
52+
> This means that these model classes are not guaranteed to be stable and may change with future releases.
53+
> They are safe to use, but may require updates even in minor releases.
54+
5055
## Getting Started
5156

5257
### What You'll Build
@@ -130,6 +135,8 @@ For more detailed information and advanced usage, please refer to the following:
130135
- [OpenAI Chat Completion](docs/guides/OPENAI_CHAT_COMPLETION.md)
131136
- [AI Core Deployment](docs/guides/AI_CORE_DEPLOYMENT.md)
132137

138+
For updating versions, please refer to the [**Release Notes**](docs/release-notes/release-notes-0-to-14.md).
139+
133140
## FAQs
134141

135142
### _"How to add a custom header to AI Core requests?"_

core/pom.xml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@
3030
</developers>
3131
<properties>
3232
<project.rootdir>${project.basedir}/../</project.rootdir>
33-
<coverage.complexity>52%</coverage.complexity>
34-
<coverage.line>64%</coverage.line>
35-
<coverage.instruction>62%</coverage.instruction>
36-
<coverage.branch>47%</coverage.branch>
37-
<coverage.method>65%</coverage.method>
38-
<coverage.class>80%</coverage.class>
33+
<coverage.complexity>62%</coverage.complexity>
34+
<coverage.line>76%</coverage.line>
35+
<coverage.instruction>76%</coverage.instruction>
36+
<coverage.branch>60%</coverage.branch>
37+
<coverage.method>74%</coverage.method>
38+
<coverage.class>90%</coverage.class>
3939
</properties>
4040

4141
<dependencies>
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package com.sap.ai.sdk.core.common;
2+
3+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
4+
import static org.mockito.ArgumentMatchers.any;
5+
import static org.mockito.Mockito.doThrow;
6+
import static org.mockito.Mockito.spy;
7+
import static org.mockito.Mockito.when;
8+
9+
import com.fasterxml.jackson.annotation.JsonProperty;
10+
import java.io.IOException;
11+
import lombok.Data;
12+
import lombok.SneakyThrows;
13+
import lombok.experimental.StandardException;
14+
import org.apache.hc.core5.http.ContentType;
15+
import org.apache.hc.core5.http.HttpEntity;
16+
import org.apache.hc.core5.http.io.entity.StringEntity;
17+
import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
18+
import org.junit.jupiter.api.Test;
19+
20+
class ClientResponseHandlerTest {
21+
22+
static class MyResponse {}
23+
24+
@Data
25+
static class MyError implements ClientError {
26+
@JsonProperty("message")
27+
private String message;
28+
}
29+
30+
@StandardException
31+
static class MyException extends ClientException {}
32+
33+
@Test
34+
public void testParseErrorAndThrow() {
35+
var sut = new ClientResponseHandler<>(MyResponse.class, MyError.class, MyException::new);
36+
37+
MyException cause = new MyException("Something wrong");
38+
39+
assertThatThrownBy(() -> sut.parseErrorAndThrow("{\"message\":\"foobar\"}", cause))
40+
.isInstanceOf(MyException.class)
41+
.hasMessage("Something wrong and error message: 'foobar'")
42+
.hasCause(cause);
43+
44+
assertThatThrownBy(() -> sut.parseErrorAndThrow("{\"foo\":\"bar\"}", cause))
45+
.isInstanceOf(MyException.class)
46+
.hasMessage("Something wrong and error message: ''")
47+
.hasCause(cause);
48+
49+
assertThatThrownBy(() -> sut.parseErrorAndThrow("<message>foobar</message>", cause))
50+
.isEqualTo(cause);
51+
}
52+
53+
@SneakyThrows
54+
@Test
55+
public void testBuildExceptionAndThrow() {
56+
var sut = new ClientResponseHandler<>(MyResponse.class, MyError.class, MyException::new);
57+
58+
HttpEntity entityWithNetworkIssues = spy(new StringEntity(""));
59+
doThrow(new IOException("Network issues")).when(entityWithNetworkIssues).writeTo(any());
60+
doThrow(new IOException("Network issues")).when(entityWithNetworkIssues).getContent();
61+
62+
var response = spy(new BasicClassicHttpResponse(400, "Bad Request"));
63+
when(response.getEntity())
64+
.thenReturn(null)
65+
.thenReturn(entityWithNetworkIssues)
66+
.thenReturn(new StringEntity("", ContentType.APPLICATION_JSON))
67+
.thenReturn(new StringEntity("<html>oh", ContentType.TEXT_HTML))
68+
.thenReturn(new StringEntity("{\"message\":\"foobar\"}", ContentType.APPLICATION_JSON));
69+
70+
assertThatThrownBy(() -> sut.buildExceptionAndThrow(response))
71+
.isInstanceOf(MyException.class)
72+
.hasMessage("Request failed with status 400 Bad Request")
73+
.hasNoCause();
74+
assertThatThrownBy(() -> sut.buildExceptionAndThrow(response))
75+
.isInstanceOf(MyException.class)
76+
.hasMessage("Request failed with status 400 Bad Request")
77+
.hasNoCause();
78+
assertThatThrownBy(() -> sut.buildExceptionAndThrow(response))
79+
.isInstanceOf(MyException.class)
80+
.hasMessage("Request failed with status 400 Bad Request")
81+
.hasNoCause();
82+
assertThatThrownBy(() -> sut.buildExceptionAndThrow(response))
83+
.isInstanceOf(MyException.class)
84+
.hasMessage("Request failed with status 400 Bad Request")
85+
.hasNoCause();
86+
assertThatThrownBy(() -> sut.buildExceptionAndThrow(response))
87+
.isInstanceOf(MyException.class)
88+
.hasMessage("Request failed with status 400 Bad Request and error message: 'foobar'")
89+
.hasCause(new MyException("Request failed with status 400 Bad Request"));
90+
}
91+
}

docs/guides/AI_CORE_DEPLOYMENT.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313

1414
This guide provides examples on how to create and manage deployments in SAP AI Core using the SAP AI SDK for Java.
1515

16+
> [!WARNING]
17+
> The below examples rely on generated model classes.
18+
> Please be aware of the [implications described here](/README.md#general-requirements).
19+
1620
## Prerequisites
1721

1822
Before using the AI Core module, ensure that you have met all the general requirements outlined in the [README.md](../../README.md#general-requirements).

docs/guides/CONNECTING_TO_AICORE.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,14 +116,18 @@ You can define a destination in the BTP Destination Service and use that to conn
116116
- **Client ID**: `[clientid]`
117117
- **Client Secret**: `[clientsecret]`
118118
- **Token Service URL Type**: `Dedicated`
119-
- **Token Service URL**: `[url]`
119+
- **Token Service URL**: `[url]/oauth/token`
120120
121121
Fill in the values for URL, client ID, client secret, and token service URL from the service key JSON.
122+
Make sure to add `/oauth/token` in the token service URL.
122123
123124
</details>
124125
125126
To use the destination, ensure you have created an instance of the BTP Destination Service and bound it to your application.
126127
128+
> [!Tip]
129+
> If you are using CAP, you can again use Hybrid Testing to bind the destination service to your application when running **locally**.
130+
127131
```java
128132
Destination destination = DestinationAccessor.getDestination("my-aicore").asHttp();
129133
AiCoreService aiCoreService = new AiCoreService().withBaseDestination(destination);

docs/guides/ORCHESTRATION_CHAT_COMPLETION.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,18 @@ var configWithFilter = config.withInputFiltering(filterStrict).withOutputFilteri
165165
var result =
166166
new OrchestrationClient().chatCompletion(prompt, configWithFilter);
167167
```
168+
#### Behavior of Input and Output Filters
169+
170+
- **Input Filter**:
171+
If the input message violates the filter policy, a `400 (Bad Request)` response will be received during the `chatCompletion` call.
172+
An `OrchestrationClientException` will be thrown.
173+
174+
- **Output Filter**:
175+
If the response message violates the output filter policy, the `chatCompletion` call will complete without exception.
176+
The convenience method `getContent()` on the resulting object will throw an `OrchestrationClientException` upon invocation.
177+
The low level API under `getOriginalResponse()` will not throw an exception.
178+
179+
You will find [some examples](../../sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java) in our Spring Boot application demonstrating response handling with filters.
168180

169181
### Data masking
170182

sample-code/spring-app/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ Before you can run the sample app, you need to install the AI SDK into your loca
1313

1414
> [!NOTE]
1515
> The sample app uses the latest state of the SDK, so make sure to install the SDK after pulling a new version via Git.
16-
> Alternatively, you check out one of the release tags of the repository, e.g. `git fetch --all --tags && git checkout rel/0.1.0`.
16+
> Alternatively, you check out one of the release tags of the repository, e.g. `git fetch --all --tags && git checkout rel/1.1.0`.
1717
1818
Next, you'll need to set up credentials for the AI Core service:
1919

20-
* Follow [these instructions](../../README.md#option-1-set-ai-core-credentials) to create a service key for the AI Core service.
20+
* Follow [these instructions](/docs/guides/CONNECTING_TO_AICORE.md) to create a service key for the AI Core service.
2121

2222
⚠️ Put the `.env` file in the sample app directory.
2323

@@ -29,4 +29,4 @@ Head to http://localhost:8080 in your browser to see all available endpoints.
2929

3030
## Run the E2E Test
3131

32-
Trigger the [GitHub Action](https://github.com/SAP/ai-sdk-java/actions/workflows/e2e-test.yml).
32+
Trigger the [GitHub Action](https://github.com/SAP/ai-sdk-java/actions/workflows/e2e-test.yaml).

sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import com.fasterxml.jackson.databind.ObjectMapper;
99
import com.sap.ai.sdk.app.services.OrchestrationService;
1010
import com.sap.ai.sdk.orchestration.AzureFilterThreshold;
11+
import com.sap.ai.sdk.orchestration.OrchestrationChatResponse;
12+
import com.sap.ai.sdk.orchestration.OrchestrationClientException;
1113
import com.sap.ai.sdk.orchestration.model.DPIEntities;
1214
import com.sap.cloud.sdk.cloudplatform.thread.ThreadContextExecutors;
1315
import javax.annotation.Nonnull;
@@ -121,30 +123,78 @@ ResponseEntity<String> messagesHistory(
121123
}
122124

123125
/**
124-
* Apply both input and output filtering for a request to orchestration.
126+
* Send an HTTP GET request for input filtering to the Orchestration service.
125127
*
126128
* @link <a
127129
* href="https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/input-filtering">SAP
128-
* AI Core: Orchestration - Input Filtering</a>
129-
* @link <a
130-
* href="https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/output-filtering">SAP
131-
* AI Core: Orchestration - Output Filtering</a>
132-
* @param policy A high threshold is a loose filter, a low threshold is a strict filter
133-
* @return a ResponseEntity with the response content
130+
* * AI Core: Orchestration - Input Filtering</a>
131+
* @param accept an optional HTTP header specifying the desired content type for the response.
132+
* @param policy path variable specifying the {@link AzureFilterThreshold} the explicitness of
133+
* content that should be allowed through the filter
134+
* @return a {@link ResponseEntity} containing the filtered input. The response is either in JSON
135+
* format if the "accept" header specifies "application/json" or in plain content format
136+
* otherwise.
137+
* @throws JsonProcessingException if an error occurs while converting the response to JSON.
134138
*/
135-
@GetMapping("/filter/{policy}")
139+
@GetMapping("/inputFiltering/{policy}")
136140
@Nonnull
137-
ResponseEntity<String> filter(
141+
ResponseEntity<String> inputFiltering(
138142
@RequestHeader(value = "accept", required = false) final String accept,
139143
@Nonnull @PathVariable("policy") final AzureFilterThreshold policy)
140144
throws JsonProcessingException {
141-
final var response = service.filter(policy, "the downtown area");
142-
if ("application/json".equals(accept)) {
145+
146+
final OrchestrationChatResponse response;
147+
try {
148+
response = service.inputFiltering(policy);
149+
} catch (OrchestrationClientException e) {
150+
final var msg = "Failed to obtain a response as the content was flagged by input filter.";
151+
log.debug(msg, e);
152+
return ResponseEntity.internalServerError().body(msg);
153+
}
154+
155+
if (accept.equals("application/json")) {
143156
return ResponseEntity.ok()
144157
.contentType(MediaType.APPLICATION_JSON)
145158
.body(mapper.writeValueAsString(response));
146159
}
147-
return ResponseEntity.ok(response.getContent());
160+
return ResponseEntity.ok().body(response.getContent());
161+
}
162+
163+
/**
164+
* Send an HTTP GET request for output filtering to the Orchestration service.
165+
*
166+
* @link <a
167+
* href="https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/output-filtering">SAP
168+
* AI Core: Orchestration - Output Filtering</a>
169+
* @param accept an optional HTTP header specifying the desired content type for the response.
170+
* @param policy a mandatory path variable specifying the {@link AzureFilterThreshold} the
171+
* explicitness of content that should be allowed through the filter
172+
* @return a {@link ResponseEntity} containing the filtered output. The response is either in JSON
173+
* format if the "accept" header specifies "application/json" or in plain content format
174+
* otherwise.
175+
* @throws OrchestrationClientException if the output filter filtered the LLM response.
176+
* @throws JsonProcessingException if an error occurs while converting the response to JSON.
177+
*/
178+
@GetMapping("/outputFiltering/{policy}")
179+
@Nonnull
180+
ResponseEntity<String> outputFiltering(
181+
@RequestHeader(value = "accept", required = false) final String accept,
182+
@Nonnull @PathVariable("policy") final AzureFilterThreshold policy)
183+
throws JsonProcessingException, OrchestrationClientException {
184+
185+
final var response = service.outputFiltering(policy);
186+
try {
187+
if (accept.equals("application/json")) {
188+
return ResponseEntity.ok()
189+
.contentType(MediaType.APPLICATION_JSON)
190+
.body(mapper.writeValueAsString(response));
191+
}
192+
return ResponseEntity.ok().body(response.getContent());
193+
} catch (OrchestrationClientException e) {
194+
final var msg = "Failed to obtain a response as the content was flagged by output filter.";
195+
log.debug(msg, e);
196+
return ResponseEntity.internalServerError().body(msg);
197+
}
148198
}
149199

150200
/**

sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import com.sap.ai.sdk.orchestration.Message;
1111
import com.sap.ai.sdk.orchestration.OrchestrationChatResponse;
1212
import com.sap.ai.sdk.orchestration.OrchestrationClient;
13+
import com.sap.ai.sdk.orchestration.OrchestrationClientException;
1314
import com.sap.ai.sdk.orchestration.OrchestrationModuleConfig;
1415
import com.sap.ai.sdk.orchestration.OrchestrationPrompt;
1516
import com.sap.ai.sdk.orchestration.model.DPIEntities;
@@ -87,34 +88,50 @@ public OrchestrationChatResponse messagesHistory(@Nonnull final String prevMessa
8788
}
8889

8990
/**
90-
* Apply both input and output filtering for a request to orchestration.
91+
* Apply input filtering for a request to orchestration.
9192
*
9293
* @link <a
9394
* href="https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/input-filtering">SAP
9495
* AI Core: Orchestration - Input Filtering</a>
96+
* @throws OrchestrationClientException if input filter filters the prompt
97+
* @param policy the explicitness of content that should be allowed through the filter
98+
* @return the assistant response object
99+
*/
100+
@Nonnull
101+
public OrchestrationChatResponse inputFiltering(@Nonnull final AzureFilterThreshold policy)
102+
throws OrchestrationClientException {
103+
final var prompt =
104+
new OrchestrationPrompt("'We shall spill blood tonight', said the operation in-charge.");
105+
final var filterConfig =
106+
new AzureContentFilter().hate(policy).selfHarm(policy).sexual(policy).violence(policy);
107+
108+
final var configWithFilter = config.withInputFiltering(filterConfig);
109+
110+
return client.chatCompletion(prompt, configWithFilter);
111+
}
112+
113+
/**
114+
* Apply output filtering for a request to orchestration.
115+
*
95116
* @link <a
96117
* href="https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/output-filtering">SAP
97118
* AI Core: Orchestration - Output Filtering</a>
98-
* @param policy A high threshold is a loose filter, a low threshold is a strict filter
119+
* @param policy the explicitness of content that should be allowed through the filter
99120
* @return the assistant response object
100121
*/
101122
@Nonnull
102-
public OrchestrationChatResponse filter(
103-
@Nonnull final AzureFilterThreshold policy, @Nonnull final String area) {
104-
final var prompt =
105-
new OrchestrationPrompt(
106-
"""
107-
Create a rental posting for subletting my apartment in %s. Keep it short. Make sure to add the following disclaimer to the end. Do not change it!
123+
public OrchestrationChatResponse outputFiltering(@Nonnull final AzureFilterThreshold policy) {
108124

109-
```DISCLAIMER: The area surrounding the apartment is known for prostitutes and gang violence including armed conflicts, gun violence is frequent.
110-
"""
111-
.formatted(area));
125+
final var systemMessage = Message.system("Give three paraphrases for the following sentence");
126+
// Reliably triggering the content filter of models fine-tuned for ethical compliance
127+
// is difficult. The prompt below may be rendered ineffective in the future.
128+
final var prompt =
129+
new OrchestrationPrompt("'We shall spill blood tonight', said the operation in-charge.")
130+
.messageHistory(List.of(systemMessage));
112131
final var filterConfig =
113132
new AzureContentFilter().hate(policy).selfHarm(policy).sexual(policy).violence(policy);
114133

115-
final var configWithFilter =
116-
config.withInputFiltering(filterConfig).withOutputFiltering(filterConfig);
117-
134+
final var configWithFilter = config.withOutputFiltering(filterConfig);
118135
return client.chatCompletion(prompt, configWithFilter);
119136
}
120137

0 commit comments

Comments
 (0)