Skip to content

Commit 285d010

Browse files
TillK17I750911bot-sdk-jsnewtorkrpanackal
authored
test: SpringAI Filtering (#403)
* added methods for filtering to SpringAiOrchestrationService.java * added filtering to SpringAiOrchestrationController.java * added filtering to Frontend * test other outputFiltering prompt * alignment of prompt to work for input and output filtering * e2e tests * Formatting * update of imports * Formatting * Revert "e2e tests" This reverts commit c2b55ab * update to support different thresholds (for testing) * tests for strict and lenient input/output filtering * detection of output filter as finish reason * fix of service * Revert "detection of output filter as finish reason" This reverts commit 88f552a. * adapted e2e tests to comply with no exception throwing * Formatting * removed exception handling from sample app * Formatting * Update sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/SpringAiOrchestrationService.java Co-authored-by: Roshin Rajan Panackal <[email protected]> * Formatting * Update SpringAiOrchestrationService.java * Formatting * drive-by spotbugs gitignore for sub-modules --------- Co-authored-by: I750911 <[email protected]> Co-authored-by: SAP Cloud SDK Bot <[email protected]> Co-authored-by: Alexander Dümont <[email protected]> Co-authored-by: Roshin Rajan Panackal <[email protected]> Co-authored-by: Alexander Dümont <[email protected]>
1 parent 4c9ca32 commit 285d010

File tree

5 files changed

+203
-1
lines changed

5 files changed

+203
-1
lines changed

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,8 @@
2828
### Environment ###
2929
*.env
3030

31-
.openapi-generator
31+
.openapi-generator
32+
33+
# sub-module spotbugs
34+
**/.pipeline/spotbugs.xml
35+
**/.pipeline/spotbugs-exclusions.xml

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
11
package com.sap.ai.sdk.app.controllers;
22

33
import com.sap.ai.sdk.app.services.SpringAiOrchestrationService;
4+
import com.sap.ai.sdk.orchestration.AzureFilterThreshold;
5+
import com.sap.ai.sdk.orchestration.OrchestrationClientException;
46
import com.sap.ai.sdk.orchestration.spring.OrchestrationSpringChatResponse;
7+
import java.util.Set;
58
import javax.annotation.Nonnull;
69
import javax.annotation.Nullable;
10+
import lombok.extern.slf4j.Slf4j;
711
import lombok.val;
812
import org.springframework.ai.chat.messages.AssistantMessage;
13+
import org.springframework.ai.chat.model.ChatResponse;
914
import org.springframework.beans.factory.annotation.Autowired;
15+
import org.springframework.http.ResponseEntity;
1016
import org.springframework.web.bind.annotation.GetMapping;
17+
import org.springframework.web.bind.annotation.PathVariable;
1118
import org.springframework.web.bind.annotation.RequestMapping;
1219
import org.springframework.web.bind.annotation.RequestParam;
1320
import org.springframework.web.bind.annotation.RestController;
1421
import reactor.core.publisher.Flux;
1522

1623
@SuppressWarnings("unused")
1724
@RestController
25+
@Slf4j
1826
@RequestMapping("/spring-ai-orchestration")
1927
class SpringAiOrchestrationController {
2028
@Autowired private SpringAiOrchestrationService service;
@@ -52,6 +60,50 @@ Object template(@Nullable @RequestParam(value = "format", required = false) fina
5260
return response.getResult().getOutput().getText();
5361
}
5462

63+
@GetMapping("/inputFiltering/{policy}")
64+
@Nonnull
65+
Object inputFiltering(
66+
@Nullable @RequestParam(value = "format", required = false) final String format,
67+
@Nonnull @PathVariable("policy") final AzureFilterThreshold policy) {
68+
69+
final ChatResponse response;
70+
try {
71+
response = service.inputFiltering(policy);
72+
} catch (OrchestrationClientException e) {
73+
final var msg = "Failed to obtain a response as the content was flagged by input filter.";
74+
log.debug(msg, e);
75+
return ResponseEntity.internalServerError().body(msg);
76+
}
77+
78+
if ("json".equals(format)) {
79+
return ((OrchestrationSpringChatResponse) response)
80+
.getOrchestrationResponse()
81+
.getOriginalResponse();
82+
}
83+
return response.getResult().getOutput().getText();
84+
}
85+
86+
@GetMapping("/outputFiltering/{policy}")
87+
@Nonnull
88+
Object outputFiltering(
89+
@Nullable @RequestParam(value = "format", required = false) final String format,
90+
@Nonnull @PathVariable("policy") final AzureFilterThreshold policy) {
91+
92+
val response = service.outputFiltering(policy);
93+
94+
if (response.hasFinishReasons(Set.of("content_filter"))) {
95+
return ResponseEntity.internalServerError()
96+
.body("Failed to obtain a response as the content was flagged by output filter.");
97+
}
98+
99+
if ("json".equals(format)) {
100+
return ((OrchestrationSpringChatResponse) response)
101+
.getOrchestrationResponse()
102+
.getOriginalResponse();
103+
}
104+
return response.getResult().getOutput().getText();
105+
}
106+
55107
@GetMapping("/masking")
56108
Object masking(@Nullable @RequestParam(value = "format", required = false) final String format) {
57109
val response = service.masking();

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

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
package com.sap.ai.sdk.app.services;
22

3+
import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.GEMINI_1_5_FLASH;
34
import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.GPT_4O_MINI;
45

6+
import com.sap.ai.sdk.orchestration.AzureContentFilter;
7+
import com.sap.ai.sdk.orchestration.AzureFilterThreshold;
58
import com.sap.ai.sdk.orchestration.DpiMasking;
9+
import com.sap.ai.sdk.orchestration.OrchestrationClientException;
610
import com.sap.ai.sdk.orchestration.OrchestrationModuleConfig;
711
import com.sap.ai.sdk.orchestration.model.DPIEntities;
812
import com.sap.ai.sdk.orchestration.spring.OrchestrationChatModel;
@@ -94,6 +98,57 @@ public ChatResponse masking() {
9498
return client.call(prompt);
9599
}
96100

101+
/**
102+
* Apply input filtering for a request to orchestration using the SpringAI integration.
103+
*
104+
* @link <a
105+
* href="https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/input-filtering">SAP
106+
* AI Core: Orchestration - Input Filtering</a>
107+
* @param policy the explicitness of content that should be allowed through the filter
108+
* @return the assistant response object
109+
*/
110+
@Nonnull
111+
public ChatResponse inputFiltering(@Nonnull final AzureFilterThreshold policy)
112+
throws OrchestrationClientException {
113+
val filterConfig =
114+
new AzureContentFilter().hate(policy).selfHarm(policy).sexual(policy).violence(policy);
115+
val opts =
116+
new OrchestrationChatOptions(
117+
config.withLlmConfig(GEMINI_1_5_FLASH).withInputFiltering(filterConfig));
118+
119+
val prompt =
120+
new Prompt(
121+
"Please rephrase the following sentence for me: 'We shall spill blood tonight', said the operator in-charge.",
122+
opts);
123+
124+
return client.call(prompt);
125+
}
126+
127+
/**
128+
* Apply output filtering for a request to orchestration using the SpringAI integration.
129+
*
130+
* @link <a
131+
* href="https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/output-filtering">SAP
132+
* AI Core: Orchestration - Output Filtering</a>
133+
* @param policy the explicitness of content that should be allowed through the filter
134+
* @return the assistant response object
135+
*/
136+
@Nonnull
137+
public ChatResponse outputFiltering(@Nonnull final AzureFilterThreshold policy) {
138+
val filterConfig =
139+
new AzureContentFilter().hate(policy).selfHarm(policy).sexual(policy).violence(policy);
140+
val opts =
141+
new OrchestrationChatOptions(
142+
config.withLlmConfig(GEMINI_1_5_FLASH).withOutputFiltering(filterConfig));
143+
144+
val prompt =
145+
new Prompt(
146+
"Please rephrase the following sentence for me: 'We shall spill blood tonight', said the operator in-charge.",
147+
opts);
148+
149+
return client.call(prompt);
150+
}
151+
97152
/**
98153
* Turn a method into a tool by annotating it with @Tool. <a
99154
* href="https://docs.spring.io/spring-ai/reference/api/tools.html#_methods_as_tools">Spring AI

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,30 @@ <h5 class="mb-1">Orchestration Integration</h5>
691691
</div>
692692
</div>
693693
</li>
694+
<li class="list-group-item">
695+
<div class="info-tooltip">
696+
<button type="submit"
697+
formaction="/spring-ai-orchestration/inputFiltering/ALLOW_SAFE"
698+
class="link-offset-2-hover link-underline link-underline-opacity-0 link-underline-opacity-75-hover endpoint">
699+
<code>/spring-ai-orchestration/inputFiltering/ALLOW_SAFE</code>
700+
</button>
701+
<div class="tooltip-content">
702+
Apply strict input filtering for a request to orchestration using the SpringAI integration.
703+
</div>
704+
</div>
705+
</li>
706+
<li class="list-group-item">
707+
<div class="info-tooltip">
708+
<button type="submit"
709+
formaction="/spring-ai-orchestration/outputFiltering/ALLOW_SAFE"
710+
class="link-offset-2-hover link-underline link-underline-opacity-0 link-underline-opacity-75-hover endpoint">
711+
<code>/spring-ai-orchestration/outputFiltering/ALLOW_SAFE</code>
712+
</button>
713+
<div class="tooltip-content">
714+
Apply strict output filtering for a request to orchestration using the SpringAI integration.
715+
</div>
716+
</div>
717+
</li>
694718
<li class="list-group-item">
695719
<div class="info-tooltip">
696720
<button type="submit"

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

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import static org.assertj.core.api.Assertions.assertThatThrownBy;
55

66
import com.sap.ai.sdk.app.services.SpringAiOrchestrationService;
7+
import com.sap.ai.sdk.orchestration.AzureFilterThreshold;
78
import com.sap.ai.sdk.orchestration.OrchestrationClientException;
9+
import com.sap.ai.sdk.orchestration.spring.OrchestrationSpringChatResponse;
810
import java.util.List;
911
import java.util.concurrent.atomic.AtomicInteger;
1012
import lombok.extern.slf4j.Slf4j;
@@ -58,6 +60,71 @@ void testMasking() {
5860
assertThat(response.getResult().getOutput().getText()).isNotEmpty();
5961
}
6062

63+
@Test
64+
void testInputFilteringStrict() {
65+
var policy = AzureFilterThreshold.ALLOW_SAFE;
66+
67+
assertThatThrownBy(() -> service.inputFiltering(policy))
68+
.isInstanceOf(OrchestrationClientException.class)
69+
.hasMessageContaining(
70+
"Content filtered due to safety violations. Please modify the prompt and try again.")
71+
.hasMessageContaining("400 Bad Request");
72+
}
73+
74+
@Test
75+
void testInputFilteringLenient() {
76+
var policy = AzureFilterThreshold.ALLOW_ALL;
77+
78+
var response = service.inputFiltering(policy);
79+
80+
assertThat(response.getResult().getMetadata().getFinishReason()).isEqualTo("stop");
81+
assertThat(response.getResult().getOutput().getText()).isNotEmpty();
82+
83+
var filterResult =
84+
((OrchestrationSpringChatResponse) response)
85+
.getOrchestrationResponse()
86+
.getOriginalResponse()
87+
.getModuleResults()
88+
.getInputFiltering();
89+
assertThat(filterResult.getMessage()).contains("passed");
90+
}
91+
92+
@Test
93+
void testOutputFilteringStrict() {
94+
var policy = AzureFilterThreshold.ALLOW_SAFE;
95+
96+
var response = service.outputFiltering(policy);
97+
98+
assertThat(response.getResult().getMetadata().getFinishReason()).isEqualTo("content_filter");
99+
assertThat(response.getResult().getOutput().getText()).isEmpty();
100+
101+
var filterResult =
102+
((OrchestrationSpringChatResponse) response)
103+
.getOrchestrationResponse()
104+
.getOriginalResponse()
105+
.getModuleResults()
106+
.getOutputFiltering();
107+
assertThat(filterResult.getMessage()).containsPattern("1 of 1 choices failed");
108+
}
109+
110+
@Test
111+
void testOutputFilteringLenient() {
112+
var policy = AzureFilterThreshold.ALLOW_ALL;
113+
114+
var response = service.outputFiltering(policy);
115+
116+
assertThat(response.getResult().getMetadata().getFinishReason()).isEqualTo("stop");
117+
assertThat(response.getResult().getOutput().getText()).isNotEmpty();
118+
119+
var filterResult =
120+
((OrchestrationSpringChatResponse) response)
121+
.getOrchestrationResponse()
122+
.getOriginalResponse()
123+
.getModuleResults()
124+
.getOutputFiltering();
125+
assertThat(filterResult.getMessage()).containsPattern("0 of \\d+ choices failed");
126+
}
127+
61128
@Test
62129
void testToolCallingWithoutExecution() {
63130
ChatResponse response = service.toolCalling(false);

0 commit comments

Comments
 (0)