Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
dd9a828
added methods for filtering to SpringAiOrchestrationService.java
Apr 3, 2025
f903634
added filtering to SpringAiOrchestrationController.java
Apr 3, 2025
e6bf06c
added filtering to Frontend
Apr 3, 2025
484d7d3
test other outputFiltering prompt
Apr 3, 2025
3174f07
alignment of prompt to work for input and output filtering
Apr 3, 2025
c2b55ab
e2e tests
Apr 3, 2025
d4e7af2
Formatting
bot-sdk-js Apr 3, 2025
b680e49
Merge branch 'main' into SpringAI-ContentFiltering-Test
TillK17 Apr 3, 2025
5c358de
update of imports
Apr 3, 2025
746a546
Formatting
bot-sdk-js Apr 3, 2025
a2fade2
Merge branch 'main' into SpringAI-ContentFiltering-Test
TillK17 Apr 3, 2025
35ac052
Revert "e2e tests"
Apr 4, 2025
0926a5d
Merge branch 'main' into SpringAI-ContentFiltering-Test
TillK17 Apr 4, 2025
dbe6dda
update to support different thresholds (for testing)
Apr 7, 2025
15d2ce9
tests for strict and lenient input/output filtering
Apr 7, 2025
88f552a
detection of output filter as finish reason
Apr 7, 2025
5281ccc
Merge remote-tracking branch 'origin/SpringAI-ContentFiltering-Test' …
Apr 7, 2025
91a990a
fix of service
Apr 7, 2025
bae6a58
Merge branch 'main' into SpringAI-ContentFiltering-Test
TillK17 Apr 7, 2025
95a61f3
Revert "detection of output filter as finish reason"
Apr 7, 2025
8e241e5
adapted e2e tests to comply with no exception throwing
Apr 7, 2025
64b7ad7
Merge branch 'main' into SpringAI-ContentFiltering-Test
TillK17 Apr 7, 2025
59ed5b2
Formatting
bot-sdk-js Apr 7, 2025
ded4a7e
removed exception handling from sample app
Apr 9, 2025
cd23ca9
Formatting
bot-sdk-js Apr 9, 2025
6c88153
Merge branch 'main' into SpringAI-ContentFiltering-Test
TillK17 Apr 9, 2025
960fbce
Merge branch 'main' into SpringAI-ContentFiltering-Test
TillK17 Apr 9, 2025
de5216f
Update sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/servic…
newtork Apr 16, 2025
ea22de2
Formatting
bot-sdk-js Apr 16, 2025
02b1e2f
Update SpringAiOrchestrationService.java
newtork Apr 16, 2025
b868c1f
Formatting
bot-sdk-js Apr 16, 2025
e7a8932
Merge remote-tracking branch 'origin/main' into SpringAI-ContentFilte…
a-d Apr 16, 2025
c6cbc7a
drive-by spotbugs gitignore for sub-modules
a-d Apr 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,8 @@
### Environment ###
*.env

.openapi-generator
.openapi-generator

# sub-module spotbugs
**/.pipeline/spotbugs.xml
**/.pipeline/spotbugs-exclusions.xml
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
package com.sap.ai.sdk.app.controllers;

import com.sap.ai.sdk.app.services.SpringAiOrchestrationService;
import com.sap.ai.sdk.orchestration.AzureFilterThreshold;
import com.sap.ai.sdk.orchestration.OrchestrationClientException;
import com.sap.ai.sdk.orchestration.spring.OrchestrationSpringChatResponse;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

@SuppressWarnings("unused")
@RestController
@Slf4j
@RequestMapping("/spring-ai-orchestration")
class SpringAiOrchestrationController {
@Autowired private SpringAiOrchestrationService service;
Expand Down Expand Up @@ -52,6 +60,50 @@ Object template(@Nullable @RequestParam(value = "format", required = false) fina
return response.getResult().getOutput().getText();
}

@GetMapping("/inputFiltering/{policy}")
@Nonnull
Object inputFiltering(
@Nullable @RequestParam(value = "format", required = false) final String format,
@Nonnull @PathVariable("policy") final AzureFilterThreshold policy) {

final ChatResponse response;
try {
response = service.inputFiltering(policy);
} catch (OrchestrationClientException e) {
final var msg = "Failed to obtain a response as the content was flagged by input filter.";
log.debug(msg, e);
return ResponseEntity.internalServerError().body(msg);
}

if ("json".equals(format)) {
return ((OrchestrationSpringChatResponse) response)
.getOrchestrationResponse()
.getOriginalResponse();
}
return response.getResult().getOutput().getText();
}

@GetMapping("/outputFiltering/{policy}")
@Nonnull
Object outputFiltering(
@Nullable @RequestParam(value = "format", required = false) final String format,
@Nonnull @PathVariable("policy") final AzureFilterThreshold policy) {

val response = service.outputFiltering(policy);

if (response.hasFinishReasons(Set.of("content_filter"))) {
return ResponseEntity.internalServerError()
.body("Failed to obtain a response as the content was flagged by output filter.");
}

if ("json".equals(format)) {
return ((OrchestrationSpringChatResponse) response)
.getOrchestrationResponse()
.getOriginalResponse();
}
return response.getResult().getOutput().getText();
}

@GetMapping("/masking")
Object masking(@Nullable @RequestParam(value = "format", required = false) final String format) {
val response = service.masking();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package com.sap.ai.sdk.app.services;

import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.GEMINI_1_5_FLASH;
import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.GPT_4O_MINI;

import com.sap.ai.sdk.orchestration.AzureContentFilter;
import com.sap.ai.sdk.orchestration.AzureFilterThreshold;
import com.sap.ai.sdk.orchestration.DpiMasking;
import com.sap.ai.sdk.orchestration.OrchestrationClientException;
import com.sap.ai.sdk.orchestration.OrchestrationModuleConfig;
import com.sap.ai.sdk.orchestration.model.DPIEntities;
import com.sap.ai.sdk.orchestration.spring.OrchestrationChatModel;
Expand Down Expand Up @@ -94,6 +98,57 @@ public ChatResponse masking() {
return client.call(prompt);
}

/**
* Apply input filtering for a request to orchestration using the SpringAI integration.
*
* @link <a
* href="https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/input-filtering">SAP
* AI Core: Orchestration - Input Filtering</a>
* @param policy the explicitness of content that should be allowed through the filter
* @return the assistant response object
*/
@Nonnull
public ChatResponse inputFiltering(@Nonnull final AzureFilterThreshold policy)
throws OrchestrationClientException {
val filterConfig =
new AzureContentFilter().hate(policy).selfHarm(policy).sexual(policy).violence(policy);
val opts =
new OrchestrationChatOptions(
config.withLlmConfig(GEMINI_1_5_FLASH).withInputFiltering(filterConfig));

val prompt =
new Prompt(
"Please rephrase the following sentence for me: 'We shall spill blood tonight', said the operator in-charge.",
opts);

return client.call(prompt);
}

/**
* Apply output filtering for a request to orchestration using the SpringAI integration.
*
* @link <a
* href="https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/output-filtering">SAP
* AI Core: Orchestration - Output Filtering</a>
* @param policy the explicitness of content that should be allowed through the filter
* @return the assistant response object
*/
@Nonnull
public ChatResponse outputFiltering(@Nonnull final AzureFilterThreshold policy) {
val filterConfig =
new AzureContentFilter().hate(policy).selfHarm(policy).sexual(policy).violence(policy);
val opts =
new OrchestrationChatOptions(
config.withLlmConfig(GEMINI_1_5_FLASH).withOutputFiltering(filterConfig));

val prompt =
new Prompt(
"Please rephrase the following sentence for me: 'We shall spill blood tonight', said the operator in-charge.",
opts);

return client.call(prompt);
}

/**
* Turn a method into a tool by annotating it with @Tool. <a
* href="https://docs.spring.io/spring-ai/reference/api/tools.html#_methods_as_tools">Spring AI
Expand Down
24 changes: 24 additions & 0 deletions sample-code/spring-app/src/main/resources/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,30 @@ <h5 class="mb-1">Orchestration Integration</h5>
</div>
</div>
</li>
<li class="list-group-item">
<div class="info-tooltip">
<button type="submit"
formaction="/spring-ai-orchestration/inputFiltering/ALLOW_SAFE"
class="link-offset-2-hover link-underline link-underline-opacity-0 link-underline-opacity-75-hover endpoint">
<code>/spring-ai-orchestration/inputFiltering/ALLOW_SAFE</code>
</button>
<div class="tooltip-content">
Apply strict input filtering for a request to orchestration using the SpringAI integration.
</div>
</div>
</li>
<li class="list-group-item">
<div class="info-tooltip">
<button type="submit"
formaction="/spring-ai-orchestration/outputFiltering/ALLOW_SAFE"
class="link-offset-2-hover link-underline link-underline-opacity-0 link-underline-opacity-75-hover endpoint">
<code>/spring-ai-orchestration/outputFiltering/ALLOW_SAFE</code>
</button>
<div class="tooltip-content">
Apply strict output filtering for a request to orchestration using the SpringAI integration.
</div>
</div>
</li>
<li class="list-group-item">
<div class="info-tooltip">
<button type="submit"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import com.sap.ai.sdk.app.services.SpringAiOrchestrationService;
import com.sap.ai.sdk.orchestration.AzureFilterThreshold;
import com.sap.ai.sdk.orchestration.OrchestrationClientException;
import com.sap.ai.sdk.orchestration.spring.OrchestrationSpringChatResponse;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -58,6 +60,71 @@ void testMasking() {
assertThat(response.getResult().getOutput().getText()).isNotEmpty();
}

@Test
void testInputFilteringStrict() {
var policy = AzureFilterThreshold.ALLOW_SAFE;

assertThatThrownBy(() -> service.inputFiltering(policy))
.isInstanceOf(OrchestrationClientException.class)
.hasMessageContaining(
"Content filtered due to safety violations. Please modify the prompt and try again.")
.hasMessageContaining("400 Bad Request");
}

@Test
void testInputFilteringLenient() {
var policy = AzureFilterThreshold.ALLOW_ALL;

var response = service.inputFiltering(policy);

assertThat(response.getResult().getMetadata().getFinishReason()).isEqualTo("stop");
assertThat(response.getResult().getOutput().getText()).isNotEmpty();

var filterResult =
((OrchestrationSpringChatResponse) response)
.getOrchestrationResponse()
.getOriginalResponse()
.getModuleResults()
.getInputFiltering();
assertThat(filterResult.getMessage()).contains("passed");
}

@Test
void testOutputFilteringStrict() {
var policy = AzureFilterThreshold.ALLOW_SAFE;

var response = service.outputFiltering(policy);

assertThat(response.getResult().getMetadata().getFinishReason()).isEqualTo("content_filter");
assertThat(response.getResult().getOutput().getText()).isEmpty();

var filterResult =
((OrchestrationSpringChatResponse) response)
.getOrchestrationResponse()
.getOriginalResponse()
.getModuleResults()
.getOutputFiltering();
assertThat(filterResult.getMessage()).containsPattern("1 of 1 choices failed");
}

@Test
void testOutputFilteringLenient() {
var policy = AzureFilterThreshold.ALLOW_ALL;

var response = service.outputFiltering(policy);

assertThat(response.getResult().getMetadata().getFinishReason()).isEqualTo("stop");
assertThat(response.getResult().getOutput().getText()).isNotEmpty();

var filterResult =
((OrchestrationSpringChatResponse) response)
.getOrchestrationResponse()
.getOriginalResponse()
.getModuleResults()
.getOutputFiltering();
assertThat(filterResult.getMessage()).containsPattern("0 of \\d+ choices failed");
}

Copy link
Contributor Author

@TillK17 TillK17 Apr 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strict tests for hitting filters and lenient tests for non-hitting filters

@Test
void testToolCallingWithoutExecution() {
ChatResponse response = service.toolCalling(false);
Expand Down