Skip to content

Commit 31546e9

Browse files
committed
Merge branch 'main' into fix-sample-app-messages
2 parents 5563547 + 4aecbbf commit 31546e9

File tree

5 files changed

+158
-51
lines changed

5 files changed

+158
-51
lines changed

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/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

sample-code/spring-app/src/main/resources/static/index.html

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -285,21 +285,23 @@ <h2>Orchestration</h2>
285285
</div>
286286
</div>
287287
</li>
288+
288289
<li class="list-group-item">
289290
<div class="info-tooltip">
290291
<a class="link-offset-2-hover link-underline link-underline-opacity-0 link-underline-opacity-75-hover endpoint"
291-
href="/orchestration/filter/ALLOW_SAFE_LOW_MEDIUM"><code>/orchestration/filter/ALLOW_SAFE_LOW_MEDIUM</code></a>
292+
href="/orchestration/inputFiltering/ALLOW_ALL"><code>/orchestration/inputFiltering/ALLOW_ALL</code></a>
292293
<div class="tooltip-content">
293-
Apply both input and output filtering for a request to orchestration with a loose filter.
294+
Apply lenient input filtering for a request to orchestration.
294295
</div>
295296
</div>
296297
</li>
297298
<li class="list-group-item">
298299
<div class="info-tooltip">
299300
<a class="link-offset-2-hover link-underline link-underline-opacity-0 link-underline-opacity-75-hover endpoint"
300-
href="/orchestration/filter/ALLOW_SAFE"><code>/orchestration/filter/ALLOW_SAFE</code></a>
301+
href="/orchestration/outputFiltering/ALLOW_ALL"><code>/orchestration/outputFiltering/ALLOW_ALL</code></a>
301302
<div class="tooltip-content">
302-
Apply both input and output filtering for a request to orchestration with a strict filter. </div>
303+
Apply lenient output filtering for a request to orchestration.
304+
</div>
303305
</div>
304306
</li>
305307
<li class="list-group-item">

sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/OrchestrationTest.java

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -100,27 +100,6 @@ void testTemplate() {
100100
assertThat(usage.getTotalTokens()).isGreaterThan(1);
101101
}
102102

103-
@Test
104-
void testLenientContentFilter() {
105-
var response = service.filter(AzureFilterThreshold.ALLOW_SAFE_LOW_MEDIUM, "the downtown area");
106-
var result = response.getOriginalResponse();
107-
var llmChoice =
108-
((LLMModuleResultSynchronous) result.getOrchestrationResult()).getChoices().get(0);
109-
assertThat(llmChoice.getFinishReason()).isEqualTo("stop");
110-
assertThat(llmChoice.getMessage().getContent()).isNotEmpty();
111-
112-
var filterResult = result.getModuleResults().getInputFiltering();
113-
assertThat(filterResult.getMessage()).contains("passed");
114-
}
115-
116-
@Test
117-
void testStrictContentFilter() {
118-
assertThatThrownBy(() -> service.filter(AzureFilterThreshold.ALLOW_SAFE, "the downtown area"))
119-
.isInstanceOf(OrchestrationClientException.class)
120-
.hasMessageContaining("400 Bad Request")
121-
.hasMessageContaining("Content filtered");
122-
}
123-
124103
@Test
125104
void testMessagesHistory() {
126105
CompletionPostResponse result =
@@ -202,4 +181,51 @@ void testCompletionWithResourceGroup() {
202181
assertThat(llmChoice.getFinishReason()).isEqualTo("stop");
203182
assertThat(llmChoice.getMessage().getContent()).isNotEmpty();
204183
}
184+
185+
@Test
186+
void testInputFilteringStrict() {
187+
var policy = AzureFilterThreshold.ALLOW_SAFE;
188+
189+
assertThatThrownBy(() -> service.inputFiltering(policy))
190+
.isInstanceOf(OrchestrationClientException.class)
191+
.hasMessageContaining(
192+
"Content filtered due to safety violations. Please modify the prompt and try again.")
193+
.hasMessageContaining("400 Bad Request");
194+
}
195+
196+
@Test
197+
void testInputFilteringLenient() {
198+
var policy = AzureFilterThreshold.ALLOW_ALL;
199+
200+
var response = service.inputFiltering(policy);
201+
202+
assertThat(response.getChoice().getFinishReason()).isEqualTo("stop");
203+
assertThat(response.getContent()).isNotEmpty();
204+
205+
var filterResult = response.getOriginalResponse().getModuleResults().getInputFiltering();
206+
assertThat(filterResult.getMessage()).contains("passed");
207+
}
208+
209+
@Test
210+
void testOutputFilteringStrict() {
211+
var policy = AzureFilterThreshold.ALLOW_SAFE;
212+
var response = service.outputFiltering(policy);
213+
214+
assertThatThrownBy(response::getContent)
215+
.isInstanceOf(OrchestrationClientException.class)
216+
.hasMessageContaining("Content filter filtered the output.");
217+
}
218+
219+
@Test
220+
void testOutputFilteringLenient() {
221+
var policy = AzureFilterThreshold.ALLOW_ALL;
222+
223+
var response = service.outputFiltering(policy);
224+
225+
assertThat(response.getChoice().getFinishReason()).isEqualTo("stop");
226+
assertThat(response.getContent()).isNotEmpty();
227+
228+
var filterResult = response.getOriginalResponse().getModuleResults().getOutputFiltering();
229+
assertThat(filterResult.getMessage()).containsPattern("0 of \\d+ choices failed");
230+
}
205231
}

0 commit comments

Comments
 (0)