Skip to content

Commit 7404e07

Browse files
feat: [Orchestration] Mask grounding (#371)
* fix: [Orchestration] Added location to server errors * wip * location only * reduce coverage for error 500 * message + location * Added test * finito * docs * fixed bug * Added unit test * Updated test * More tests * Used lombok @with * Update docs/guides/ORCHESTRATION_CHAT_COMPLETION.md --------- Co-authored-by: Jonas-Isr <[email protected]>
1 parent 6b0ff45 commit 7404e07

File tree

12 files changed

+120
-15
lines changed

12 files changed

+120
-15
lines changed

docs/guides/ORCHESTRATION_CHAT_COMPLETION.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,23 @@ var response = client.chatCompletion(prompt, configWithGrounding);
271271

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

274+
### Mask Grounding
275+
276+
You can also mask both the grounding information and the prompt message:
277+
278+
```java
279+
var maskingConfig =
280+
DpiMasking.anonymization()
281+
.withEntities(DPIEntities.SENSITIVE_DATA)
282+
.withMaskGroundingEnabled()
283+
.withAllowList(List.of("SAP", "Joule"));
284+
var maskedGroundingConfig = groundingConfig.withMaskingConfig(maskingConfig);
285+
286+
var result = client.chatCompletion(prompt, maskedGroundingConfig);
287+
```
288+
289+
Please find [an example in our Spring Boot application](../../sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java).
290+
274291
## Stream chat completion
275292

276293
It's possible to pass a stream of chat completion delta elements, e.g. from the application backend to the frontend in real-time.

docs/release-notes/release_notes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
- [Orchestration] [Add Spring AI Chat Memory support](https://github.com/SAP/ai-sdk-java/tree/main/docs/guides/SPRING_AI_INTEGRATION.md#chat-memory)
1616
- [Orchestration] [Prompt templates can be consumed from registry.](https://github.com/SAP/ai-sdk-java/tree/main/docs/guides/ORCHESTRATION_CHAT_COMPLETION.md#Chat-completion-with-Templates)
17+
- [Orchestration] [Masking is now available on grounding.](https://github.com/SAP/ai-sdk-java/tree/main/docs/guides/ORCHESTRATION_CHAT_COMPLETION.md#mask-grounding)
1718
- [Orchestration] [Grounding via *help.sap.com* is enabled.](https://github.com/SAP/ai-sdk-java/tree/main/docs/guides/ORCHESTRATION_CHAT_COMPLETION.md#grounding)
1819

1920
### 📈 Improvements

orchestration/src/main/java/com/sap/ai/sdk/orchestration/DpiMasking.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import static com.sap.ai.sdk.orchestration.model.DPIConfig.TypeEnum.SAP_DATA_PRIVACY_INTEGRATION;
66

77
import com.sap.ai.sdk.orchestration.model.DPIConfig;
8+
import com.sap.ai.sdk.orchestration.model.DPIConfigMaskGroundingInput;
89
import com.sap.ai.sdk.orchestration.model.DPIEntities;
910
import com.sap.ai.sdk.orchestration.model.DPIEntityConfig;
1011
import com.sap.ai.sdk.orchestration.model.MaskingProviderConfig;
@@ -16,6 +17,7 @@
1617
import lombok.Getter;
1718
import lombok.RequiredArgsConstructor;
1819
import lombok.Value;
20+
import lombok.With;
1921
import lombok.val;
2022

2123
/**
@@ -31,6 +33,8 @@
3133
public class DpiMasking implements MaskingProvider {
3234
@Nonnull DPIConfig.MethodEnum maskingMethod;
3335
@Nonnull List<DPIEntities> entities;
36+
@With boolean maskGroundingInput;
37+
@Nonnull List<String> allowList;
3438

3539
/**
3640
* Build a configuration applying anonymization.
@@ -65,7 +69,7 @@ public static class Builder {
6569
*
6670
* @param entity An entity type to mask (required)
6771
* @param entities Additional entity types to mask (optional)
68-
* @return A configured {@link DpiMasking} instance
72+
* @return A new {@link DpiMasking} instance
6973
* @see DPIEntities
7074
*/
7175
@Nonnull
@@ -74,17 +78,30 @@ public DpiMasking withEntities(
7478
val entitiesList = new ArrayList<DPIEntities>();
7579
entitiesList.add(entity);
7680
entitiesList.addAll(Arrays.asList(entities));
77-
return new DpiMasking(maskingMethod, entitiesList);
81+
return new DpiMasking(maskingMethod, entitiesList, false, List.of());
7882
}
7983
}
8084

85+
/**
86+
* Set words that should not be masked.
87+
*
88+
* @param allowList List of strings that should not be masked
89+
* @return A new {@link DpiMasking} instance
90+
*/
91+
@Nonnull
92+
public DpiMasking withAllowList(@Nonnull final List<String> allowList) {
93+
return new DpiMasking(maskingMethod, entities, maskGroundingInput, allowList);
94+
}
95+
8196
@Nonnull
8297
@Override
8398
public MaskingProviderConfig createConfig() {
8499
val entitiesDTO = entities.stream().map(it -> DPIEntityConfig.create().type(it)).toList();
85100
return DPIConfig.create()
86101
.type(SAP_DATA_PRIVACY_INTEGRATION)
87102
.method(maskingMethod)
88-
.entities(entitiesDTO);
103+
.entities(entitiesDTO)
104+
.maskGroundingInput(DPIConfigMaskGroundingInput.create().enabled(maskGroundingInput))
105+
.allowlist(allowList);
89106
}
90107
}

orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfigTest.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,11 @@ void testThrowOnEmptyFilterConfig() {
7777

7878
@Test
7979
void testDpiMaskingConfig() {
80-
var maskingConfig = DpiMasking.anonymization().withEntities(DPIEntities.ADDRESS);
80+
var maskingConfig =
81+
DpiMasking.anonymization()
82+
.withEntities(DPIEntities.ADDRESS)
83+
.withMaskGroundingInput(true)
84+
.withAllowList(List.of("Alice"));
8185
var config =
8286
new OrchestrationModuleConfig().withLlmConfig(GPT_4O).withMaskingConfig(maskingConfig);
8387

@@ -87,6 +91,8 @@ void testDpiMaskingConfig() {
8791
assertThat(dpiConfig.getMethod()).isEqualTo(DPIConfig.MethodEnum.ANONYMIZATION);
8892
assertThat(dpiConfig.getEntities()).hasSize(1);
8993
assertThat(dpiConfig.getEntities().get(0).getType()).isEqualTo(DPIEntities.ADDRESS);
94+
assertThat(dpiConfig.getMaskGroundingInput().isEnabled()).isEqualTo(true);
95+
assertThat(dpiConfig.getAllowlist()).containsExactly("Alice");
9096

9197
var configModified = config.withMaskingConfig(maskingConfig);
9298
assertThat(configModified.getMaskingConfig()).isNotNull();

orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,13 @@ void testGrounding() throws IOException {
170170
GroundingModuleConfig.create()
171171
.type(GroundingModuleConfig.TypeEnum.DOCUMENT_GROUNDING_SERVICE)
172172
.config(groundingConfigConfig);
173-
final var configWithGrounding = config.withGroundingConfig(groundingConfig);
173+
val maskingConfig = // optional masking configuration
174+
DpiMasking.anonymization()
175+
.withEntities(DPIEntities.SENSITIVE_DATA)
176+
.withMaskGroundingInput(true)
177+
.withAllowList(List.of("SAP", "Joule"));
178+
final var configWithGrounding =
179+
config.withGroundingConfig(groundingConfig).withMaskingConfig(maskingConfig);
174180

175181
final Map<String, String> inputParams =
176182
Map.of("query", "String used for similarity search in database");
@@ -200,6 +206,18 @@ void testGrounding() throws IOException {
200206
"First chunk```Second chunk```Last found chunk");
201207
assertThat(groundingModule.getData()).isEqualTo(groundingData);
202208

209+
var inputMasking = response.getOriginalResponse().getModuleResults().getInputMasking();
210+
assertThat(inputMasking.getMessage())
211+
.isEqualTo("Input to LLM and Grounding is masked successfully.");
212+
Object data = inputMasking.getData();
213+
assertThat(data)
214+
.isEqualTo(
215+
Map.of(
216+
"masked_template",
217+
"[{\"role\": \"user\", \"content\": \"Context message with embedded grounding results. First chunk```MASKED_SENSITIVE_DATA```Last found chunk\"}]",
218+
"masked_grounding_input", // maskGroundingInput: true will make this field present
219+
"[\"What does Joule do?\"]"));
220+
203221
try (var requestInputStream = fileLoader.apply("groundingRequest.json")) {
204222
final String request = new String(requestInputStream.readAllBytes());
205223
verify(postRequestedFor(urlPathEqualTo("/completion")).withRequestBody(equalToJson(request)));

orchestration/src/test/resources/__files/groundingResponse.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@
1414
"content": "Context message with embedded grounding results. First chunk```Second chunk```Last found chunk"
1515
}
1616
],
17+
"input_masking": {
18+
"message": "Input to LLM and Grounding is masked successfully.",
19+
"data": {
20+
"masked_template": "[{\"role\": \"user\", \"content\": \"Context message with embedded grounding results. First chunk```MASKED_SENSITIVE_DATA```Last found chunk\"}]",
21+
"masked_grounding_input": "[\"What does Joule do?\"]"
22+
}
23+
},
1724
"llm": {
1825
"id": "chatcmpl-Apz5s3CMf99jkOnxvPshH1rGLwvvU",
1926
"object": "chat.completion",
@@ -59,4 +66,4 @@
5966
"total_tokens": 380
6067
}
6168
}
62-
}
69+
}

orchestration/src/test/resources/groundingRequest.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,19 @@
2121
"defaults" : { },
2222
"tools" : [ ]
2323
},
24+
"masking_module_config" : {
25+
"masking_providers" : [ {
26+
"type" : "sap_data_privacy_integration",
27+
"method" : "anonymization",
28+
"entities" : [ {
29+
"type" : "profile-sensitive-data"
30+
} ],
31+
"allowlist" : [ "SAP", "Joule" ],
32+
"mask_grounding_input" : {
33+
"enabled" : true
34+
}
35+
} ]
36+
},
2437
"grounding_module_config" : {
2538
"type" : "document_grounding_service",
2639
"config" : {
@@ -53,4 +66,4 @@
5366
"query" : "String used for similarity search in database"
5467
},
5568
"messages_history" : [ ]
56-
}
69+
}

orchestration/src/test/resources/maskingRequest.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@
3333
"type": "profile-phone"
3434
}
3535
],
36-
"allowlist" : [ ]
36+
"allowlist" : [ ],
37+
"mask_grounding_input" : {
38+
"enabled" : false
39+
}
3740
}
3841
]
3942
}

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,11 +180,12 @@ Object maskingPseudonymization(
180180
return response.getContent();
181181
}
182182

183-
@GetMapping("/grounding")
183+
@GetMapping("/grounding/{maskGroundingInput}")
184184
@Nonnull
185185
Object grounding(
186-
@Nullable @RequestParam(value = "format", required = false) final String format) {
187-
final var response = service.grounding("What does Joule do?");
186+
@Nullable @RequestParam(value = "format", required = false) final String format,
187+
@PathVariable("maskGroundingInput") final boolean maskGroundingInput) {
188+
final var response = service.grounding("What does Joule do?", maskGroundingInput);
188189
if ("json".equals(format)) {
189190
return response;
190191
}

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -316,10 +316,12 @@ public OrchestrationChatResponse maskingPseudonymization(@Nonnull final DPIEntit
316316
* @link <a href="https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/grounding">SAP
317317
* AI Core: Orchestration - Grounding</a>
318318
* @param userMessage the user message to provide grounding for
319+
* @param maskGroundingInput whether to mask the request sent to the Grounding Service
319320
* @return the assistant response object
320321
*/
321322
@Nonnull
322-
public OrchestrationChatResponse grounding(@Nonnull final String userMessage) {
323+
public OrchestrationChatResponse grounding(
324+
@Nonnull final String userMessage, final boolean maskGroundingInput) {
323325
// optional filter for collections
324326
val documentMetadata =
325327
SearchDocumentKeyValueListPair.create()
@@ -335,7 +337,13 @@ public OrchestrationChatResponse grounding(@Nonnull final String userMessage) {
335337

336338
val groundingConfig = Grounding.create().filters(databaseFilter);
337339
val prompt = groundingConfig.createGroundingPrompt(userMessage);
338-
val configWithGrounding = config.withGrounding(groundingConfig);
340+
val maskingConfig = // optional masking configuration
341+
DpiMasking.anonymization()
342+
.withEntities(DPIEntities.SENSITIVE_DATA)
343+
.withMaskGroundingInput(maskGroundingInput)
344+
.withAllowList(List.of("SAP", "Joule"));
345+
val configWithGrounding =
346+
config.withGrounding(groundingConfig).withMaskingConfig(maskingConfig);
339347

340348
return client.chatCompletion(prompt, configWithGrounding);
341349
}

0 commit comments

Comments
 (0)