Skip to content

Commit 00de28f

Browse files
n-o-u-r-h-a-nCharlesDuboisSAPbot-sdk-jsnewtork
authored
chore: Support new Data Masking Config (#592)
* Supporting DPICustomEntity and option field replacement_strategy * Update sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java Co-authored-by: Charles Dubois <[email protected]> * Fixing PR comments * Formatting * Updates * Adding Release Notes. * Updating withEntities method + Implementing withRegex() multiple times in a row. * pom * Updating code of new withRegex function to make it more efficient. * Formatting * Removing unused import. * Updated release notes * Updating unit test for DpiMasking * Formatting * Formatting opened files * Improving code quality (reducing indentations) of withRegex() function outside builder. * Fixing Build * Updating Unit Test * Reverting coverage percentage. * Updating javadoc of maskingRegex() function. * Updating again the javadoc of maskingRegex() function. * Splitting Test and updating minor comments. * Splitting 2 Test and updating minor comments. * Update docs/release_notes.md Co-authored-by: Alexander Dümont <[email protected]> * Updating test name. --------- Co-authored-by: Charles Dubois <[email protected]> Co-authored-by: SAP Cloud SDK Bot <[email protected]> Co-authored-by: Alexander Dümont <[email protected]>
1 parent 75be5ef commit 00de28f

File tree

7 files changed

+152
-14
lines changed

7 files changed

+152
-14
lines changed

docs/release_notes.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555

5656
### 📈 Improvements
5757

58-
-
58+
- [Orchestration] Added new API `DpiMasking#withRegex` to apply custom masking patterns.
5959

6060
### 🐛 Fixed Issues
6161

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

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66

77
import com.sap.ai.sdk.orchestration.model.DPIConfig;
88
import com.sap.ai.sdk.orchestration.model.DPIConfigMaskGroundingInput;
9+
import com.sap.ai.sdk.orchestration.model.DPICustomEntity;
910
import com.sap.ai.sdk.orchestration.model.DPIEntities;
1011
import com.sap.ai.sdk.orchestration.model.DPIEntityConfig;
12+
import com.sap.ai.sdk.orchestration.model.DPIMethodConstant;
1113
import com.sap.ai.sdk.orchestration.model.DPIStandardEntity;
12-
import java.util.ArrayList;
1314
import java.util.Arrays;
1415
import java.util.List;
16+
import java.util.stream.Stream;
1517
import javax.annotation.Nonnull;
1618
import lombok.AccessLevel;
1719
import lombok.Getter;
@@ -32,7 +34,7 @@
3234
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
3335
public class DpiMasking implements MaskingProvider {
3436
@Nonnull DPIConfig.MethodEnum maskingMethod;
35-
@Nonnull List<DPIEntities> entities;
37+
@Nonnull List<DPIEntityConfig> entitiesConfig;
3638
@With boolean maskGroundingInput;
3739
@Nonnull List<String> allowList;
3840

@@ -75,11 +77,53 @@ public static class Builder {
7577
@Nonnull
7678
public DpiMasking withEntities(
7779
@Nonnull final DPIEntities entity, @Nonnull final DPIEntities... entities) {
78-
val entitiesList = new ArrayList<DPIEntities>();
79-
entitiesList.add(entity);
80-
entitiesList.addAll(Arrays.asList(entities));
81-
return new DpiMasking(maskingMethod, entitiesList, false, List.of());
80+
val entitiesConfig =
81+
Stream.concat(Stream.of(entity), Arrays.stream(entities))
82+
.map(it -> (DPIEntityConfig) DPIStandardEntity.create().type(it))
83+
.toList();
84+
return new DpiMasking(maskingMethod, entitiesConfig, false, List.of());
8285
}
86+
87+
/**
88+
* Adds a custom regex pattern for masking.
89+
*
90+
* @param regex The regex pattern to match
91+
* @param replacement The replacement string
92+
* @return A new {@link DpiMasking} instance
93+
*/
94+
@Nonnull
95+
public DpiMasking withRegex(@Nonnull final String regex, @Nonnull final String replacement) {
96+
val customEntity =
97+
DPICustomEntity.create()
98+
.regex(regex)
99+
.replacementStrategy(
100+
DPIMethodConstant.create()
101+
.method(DPIMethodConstant.MethodEnum.CONSTANT)
102+
.value(replacement));
103+
104+
return new DpiMasking(maskingMethod, List.of(customEntity), false, List.of());
105+
}
106+
}
107+
108+
/**
109+
* Specifies a custom regex pattern for masking.
110+
*
111+
* @param regex The regex pattern to match
112+
* @param replacement The replacement string
113+
* @return A new {@link DpiMasking} instance
114+
*/
115+
@Nonnull
116+
public DpiMasking withRegex(@Nonnull final String regex, @Nonnull final String replacement) {
117+
val customEntity =
118+
DPICustomEntity.create()
119+
.regex(regex)
120+
.replacementStrategy(
121+
DPIMethodConstant.create()
122+
.method(DPIMethodConstant.MethodEnum.CONSTANT)
123+
.value(replacement));
124+
val newEntities = new java.util.ArrayList<>(entitiesConfig);
125+
newEntities.add(customEntity);
126+
return new DpiMasking(maskingMethod, newEntities, maskGroundingInput, allowList);
83127
}
84128

85129
/**
@@ -90,18 +134,16 @@ public DpiMasking withEntities(
90134
*/
91135
@Nonnull
92136
public DpiMasking withAllowList(@Nonnull final List<String> allowList) {
93-
return new DpiMasking(maskingMethod, entities, maskGroundingInput, allowList);
137+
return new DpiMasking(maskingMethod, entitiesConfig, maskGroundingInput, allowList);
94138
}
95139

96140
@Nonnull
97141
@Override
98142
public DPIConfig createConfig() {
99-
val entitiesDTO =
100-
entities.stream().map(it -> (DPIEntityConfig) DPIStandardEntity.create().type(it)).toList();
101143
return DPIConfig.create()
102144
.type(SAP_DATA_PRIVACY_INTEGRATION)
103145
.method(maskingMethod)
104-
.entities(entitiesDTO)
146+
.entities(entitiesConfig)
105147
.maskGroundingInput(DPIConfigMaskGroundingInput.create().enabled(maskGroundingInput))
106148
.allowlist(allowList);
107149
}

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010

1111
import com.fasterxml.jackson.annotation.JsonProperty;
1212
import com.sap.ai.sdk.orchestration.model.DPIConfig;
13+
import com.sap.ai.sdk.orchestration.model.DPICustomEntity;
1314
import com.sap.ai.sdk.orchestration.model.DPIEntities;
15+
import com.sap.ai.sdk.orchestration.model.DPIMethodConstant;
1416
import com.sap.ai.sdk.orchestration.model.DPIStandardEntity;
1517
import com.sap.ai.sdk.orchestration.model.DocumentGroundingFilter;
1618
import com.sap.ai.sdk.orchestration.model.GroundingModuleConfigConfig;
@@ -106,6 +108,36 @@ void testDpiMaskingConfig() {
106108
.hasSize(1);
107109
}
108110

111+
@Test
112+
void testDpiMaskingRegex() {
113+
var masking =
114+
DpiMasking.anonymization()
115+
.withRegex("\\d{3}-\\d{2}-\\d{4}", "***-**-****")
116+
.withRegex("\\d{2}-\\d{2}-\\d{5}", "**-**-*****");
117+
var config = new OrchestrationModuleConfig().withLlmConfig(GPT_4O).withMaskingConfig(masking);
118+
assertThat(config.getMaskingConfig()).isNotNull();
119+
assertThat(((MaskingModuleConfigProviders) config.getMaskingConfig()).getProviders())
120+
.hasSize(1);
121+
DPIConfig dpiConfig =
122+
((MaskingModuleConfigProviders) config.getMaskingConfig()).getProviders().get(0);
123+
assertThat(dpiConfig.getMethod()).isEqualTo(DPIConfig.MethodEnum.ANONYMIZATION);
124+
assertThat(dpiConfig.getEntities()).hasSize(2);
125+
assertThat(((DPICustomEntity) dpiConfig.getEntities().get(0)).getRegex())
126+
.isEqualTo("\\d{3}-\\d{2}-\\d{4}");
127+
assertThat(((DPICustomEntity) dpiConfig.getEntities().get(0)).getReplacementStrategy())
128+
.isEqualTo(
129+
DPIMethodConstant.create()
130+
.method(DPIMethodConstant.MethodEnum.CONSTANT)
131+
.value("***-**-****"));
132+
assertThat(((DPICustomEntity) dpiConfig.getEntities().get(1)).getRegex())
133+
.isEqualTo("\\d{2}-\\d{2}-\\d{5}");
134+
assertThat(((DPICustomEntity) dpiConfig.getEntities().get(1)).getReplacementStrategy())
135+
.isEqualTo(
136+
DPIMethodConstant.create()
137+
.method(DPIMethodConstant.MethodEnum.CONSTANT)
138+
.value("**-**-*****"));
139+
}
140+
109141
@Test
110142
void testParams() {
111143
// test withParams(Map<String, Object>)

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,17 @@ Object maskingAnonymization(
216216
return response.getContent();
217217
}
218218

219+
@GetMapping("/maskingRegex")
220+
@Nonnull
221+
Object maskingRegex(
222+
@Nullable @RequestParam(value = "format", required = false) final String format) {
223+
final var response = service.maskingRegex();
224+
if ("json".equals(format)) {
225+
return response;
226+
}
227+
return response.getContent();
228+
}
229+
219230
@GetMapping("/completion/{resourceGroup}")
220231
@Nonnull
221232
Object completionWithResourceGroup(

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

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -267,9 +267,9 @@ public OrchestrationChatResponse maskingAnonymization(@Nonnull final DPIEntities
267267
val userMessage =
268268
Message.user(
269269
"""
270-
I think the SDK is good, but could use some further enhancements.
271-
My architect Alice and manager Bob pointed out that we need the grounding capabilities, which aren't supported yet.
272-
""");
270+
I think the SDK is good, but could use some further enhancements.
271+
My architect Alice and manager Bob pointed out that we need the grounding capabilities, which aren't supported yet.
272+
""");
273273

274274
val prompt = new OrchestrationPrompt(systemMessage, userMessage);
275275
val maskingConfig = DpiMasking.anonymization().withEntities(entity);
@@ -278,6 +278,28 @@ public OrchestrationChatResponse maskingAnonymization(@Nonnull final DPIEntities
278278
return client.chatCompletion(prompt, configWithMasking);
279279
}
280280

281+
/**
282+
* Let the LLM respond with a masked repeated phrase of patient IDs.
283+
*
284+
* @link <a
285+
* href="https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/data-masking">SAP AI
286+
* Core: Orchestration - Data Masking</a>
287+
* @return the assistant response object
288+
*/
289+
@Nonnull
290+
public OrchestrationChatResponse maskingRegex() {
291+
val systemMessage = Message.system("Repeat following messages");
292+
val userMessage = Message.user("The patient id is patient_id_123.");
293+
294+
val prompt = new OrchestrationPrompt(systemMessage, userMessage);
295+
val regex = "patient_id_[0-9]+";
296+
val replacement = "REDACTED_ID";
297+
val maskingConfig = DpiMasking.anonymization().withRegex(regex, replacement);
298+
val configWithMasking = config.withMaskingConfig(maskingConfig);
299+
300+
return client.chatCompletion(prompt, configWithMasking);
301+
}
302+
281303
/**
282304
* Chat request to OpenAI through the Orchestration deployment under a specific resource group.
283305
*

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,18 @@ <h2>Orchestration</h2>
380380
</div>
381381
</div>
382382
</li>
383+
<li class="list-group-item">
384+
<div class="info-tooltip">
385+
<button type="submit"
386+
formaction="/orchestration/maskingRegex"
387+
class="link-offset-2-hover link-underline link-underline-opacity-0 link-underline-opacity-75-hover endpoint">
388+
<code>/orchestration/maskingRegex</code>
389+
</button>
390+
<div class="tooltip-content">
391+
Let the LLM respond with a masked repeated phrase of patient IDs.
392+
</div>
393+
</div>
394+
</li>
383395
<li class="list-group-item">
384396
<div class="info-tooltip">
385397
<button type="submit"

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,25 @@ void testMaskingAnonymization() {
150150
assertThat(result.getIntermediateResults().getOutputUnmasking()).isEmpty();
151151
}
152152

153+
@Test
154+
void testMaskingRegex() {
155+
var response = service.maskingRegex();
156+
var result = response.getOriginalResponse();
157+
var llmChoice = result.getFinalResult().getChoices().get(0);
158+
assertThat(llmChoice.getFinishReason()).isEqualTo("stop");
159+
160+
var maskingResult = result.getIntermediateResults().getInputMasking();
161+
assertThat(maskingResult.getMessage()).isNotEmpty();
162+
var data = (Map<String, Object>) maskingResult.getData();
163+
var maskedMessage = (String) data.get("masked_template");
164+
assertThat(maskedMessage)
165+
.describedAs("The masked input should replace patient IDs with REDACTED_ID")
166+
.doesNotContain("patient_id_123")
167+
.contains("REDACTED_ID");
168+
169+
assertThat(result.getIntermediateResults().getOutputUnmasking()).isEmpty();
170+
}
171+
153172
@SuppressWarnings("unchecked")
154173
@Test
155174
void testMaskingPseudonymization() {

0 commit comments

Comments
 (0)