Skip to content

Commit 5e57895

Browse files
Data Masking Convenience (#172)
* Data Masking Convenience * Created interface MaskingProvider * Added unit test * Drive by tests * Fix docs * Update orchestration/src/main/java/com/sap/ai/sdk/orchestration/DpiMasking.java * Update orchestration/src/main/java/com/sap/ai/sdk/orchestration/DpiMasking.java * Update docs/guides/ORCHESTRATION_CHAT_COMPLETION.md --------- Co-authored-by: Matthias Kuhr <[email protected]>
1 parent b572c41 commit 5e57895

File tree

8 files changed

+185
-62
lines changed

8 files changed

+185
-62
lines changed

docs/guides/ORCHESTRATION_CHAT_COMPLETION.md

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -184,14 +184,8 @@ var result =
184184
Use the data masking module to anonymize personal information in the input:
185185

186186
```java
187-
var maskingProvider =
188-
MaskingProviderConfig.create()
189-
.type(MaskingProviderConfig.TypeEnum.SAP_DATA_PRIVACY_INTEGRATION)
190-
.method(MaskingProviderConfig.MethodEnum.ANONYMIZATION)
191-
.entities(
192-
DPIEntityConfig.create().type(DPIEntities.PHONE),
193-
DPIEntityConfig.create().type(DPIEntities.PERSON));
194-
var maskingConfig = MaskingModuleConfig.create().maskingProviders(maskingProvider);
187+
var maskingConfig =
188+
DpiMasking.anonymization().withEntities(DPIEntities.PHONE, DPIEntities.PERSON);
195189
var configWithMasking = config.withMaskingConfig(maskingConfig);
196190

197191
var systemMessage = ChatMessage.create()
@@ -210,7 +204,7 @@ var result =
210204
new OrchestrationClient().chatCompletion(prompt, configWithMasking);
211205
```
212206

213-
In this example, the input will be masked before the call to the LLM. Note that data cannot be unmasked in the LLM output.
207+
In this example, the input will be masked before the call to the LLM and will remain masked in the output.
214208

215209
### Set model parameters
216210

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package com.sap.ai.sdk.orchestration;
2+
3+
import static com.sap.ai.sdk.orchestration.client.model.DPIConfig.MethodEnum.ANONYMIZATION;
4+
import static com.sap.ai.sdk.orchestration.client.model.DPIConfig.MethodEnum.PSEUDONYMIZATION;
5+
import static com.sap.ai.sdk.orchestration.client.model.DPIConfig.TypeEnum.SAP_DATA_PRIVACY_INTEGRATION;
6+
7+
import com.sap.ai.sdk.orchestration.client.model.DPIConfig;
8+
import com.sap.ai.sdk.orchestration.client.model.DPIEntities;
9+
import com.sap.ai.sdk.orchestration.client.model.DPIEntityConfig;
10+
import com.sap.ai.sdk.orchestration.client.model.MaskingProviderConfig;
11+
import java.util.ArrayList;
12+
import java.util.Arrays;
13+
import java.util.List;
14+
import javax.annotation.Nonnull;
15+
import lombok.AccessLevel;
16+
import lombok.Getter;
17+
import lombok.RequiredArgsConstructor;
18+
import lombok.Value;
19+
import lombok.val;
20+
21+
/**
22+
* SAP Data Privacy Integration (DPI) can mask personally identifiable information using either
23+
* anonymization or pseudonymization.
24+
*/
25+
@Value
26+
@Getter(AccessLevel.PACKAGE)
27+
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
28+
public class DpiMasking implements MaskingProvider {
29+
@Nonnull DPIConfig.MethodEnum maskingMethod;
30+
@Nonnull List<DPIEntities> entities;
31+
32+
/**
33+
* Build a configuration applying anonymization.
34+
*
35+
* @return A builder configured for anonymization
36+
*/
37+
@Nonnull
38+
public static Builder anonymization() {
39+
return new DpiMasking.Builder(ANONYMIZATION);
40+
}
41+
42+
/**
43+
* Build a configuration applying pseudonymization.
44+
*
45+
* @return A builder configured for pseudonymization
46+
*/
47+
@Nonnull
48+
public static Builder pseudonymization() {
49+
return new DpiMasking.Builder(PSEUDONYMIZATION);
50+
}
51+
52+
/**
53+
* Builder for creating DPI masking configurations. Allows specifying which entity types should be
54+
* masked in the input text.
55+
*/
56+
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
57+
public static class Builder {
58+
private final DPIConfig.MethodEnum maskingMethod;
59+
60+
/**
61+
* Specifies which entities should be masked in the input text.
62+
*
63+
* @param entity An entity type to mask (required)
64+
* @param entities Additional entity types to mask (optional)
65+
* @return A configured {@link DpiMasking} instance
66+
* @see DPIEntities
67+
*/
68+
@Nonnull
69+
public DpiMasking withEntities(
70+
@Nonnull final DPIEntities entity, @Nonnull final DPIEntities... entities) {
71+
val entitiesList = new ArrayList<DPIEntities>();
72+
entitiesList.add(entity);
73+
entitiesList.addAll(Arrays.asList(entities));
74+
return new DpiMasking(maskingMethod, entitiesList);
75+
}
76+
}
77+
78+
@Nonnull
79+
@Override
80+
public MaskingProviderConfig createConfig() {
81+
val entitiesDTO = entities.stream().map(it -> new DPIEntityConfig().type(it)).toList();
82+
return new DPIConfig()
83+
.type(SAP_DATA_PRIVACY_INTEGRATION)
84+
.method(maskingMethod)
85+
.entities(entitiesDTO);
86+
}
87+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.sap.ai.sdk.orchestration;
2+
3+
import com.sap.ai.sdk.orchestration.client.model.MaskingProviderConfig;
4+
import javax.annotation.Nonnull;
5+
6+
/** Interface for masking configurations. */
7+
public interface MaskingProvider {
8+
9+
/**
10+
* Create a masking provider for the configuration.
11+
*
12+
* @return the masking provider
13+
*/
14+
@Nonnull
15+
MaskingProviderConfig createConfig();
16+
}

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.sap.ai.sdk.orchestration.client.model.LLMModuleConfig;
55
import com.sap.ai.sdk.orchestration.client.model.MaskingModuleConfig;
66
import com.sap.ai.sdk.orchestration.client.model.TemplatingModuleConfig;
7+
import java.util.Arrays;
78
import javax.annotation.Nonnull;
89
import javax.annotation.Nullable;
910
import lombok.AccessLevel;
@@ -12,6 +13,7 @@
1213
import lombok.Value;
1314
import lombok.With;
1415
import lombok.experimental.Tolerate;
16+
import lombok.val;
1517

1618
/**
1719
* Represents the configuration for the orchestration service. Allows for configuring the different
@@ -62,4 +64,24 @@ public class OrchestrationModuleConfig {
6264
public OrchestrationModuleConfig withLlmConfig(@Nonnull final OrchestrationAiModel aiModel) {
6365
return withLlmConfig(aiModel.createConfig());
6466
}
67+
68+
/**
69+
* Creates a new configuration with the given Data Masking configuration.
70+
*
71+
* @param maskingProvider The Data Masking configuration to use.
72+
* @param maskingProviders Additional Data Masking configurations to use.
73+
* @return A new configuration with the given Data Masking configuration.
74+
*/
75+
@Tolerate
76+
@Nonnull
77+
public OrchestrationModuleConfig withMaskingConfig(
78+
@Nonnull final MaskingProvider maskingProvider,
79+
@Nonnull final MaskingProvider... maskingProviders) {
80+
val newMaskingConfig =
81+
new MaskingModuleConfig().addMaskingProvidersItem(maskingProvider.createConfig());
82+
Arrays.stream(maskingProviders)
83+
.forEach(it -> newMaskingConfig.addMaskingProvidersItem(it.createConfig()));
84+
85+
return withMaskingConfig(newMaskingConfig);
86+
}
6587
}

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package com.sap.ai.sdk.orchestration;
22

3+
import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.GPT_4O;
34
import static com.sap.ai.sdk.orchestration.OrchestrationUnitTest.CUSTOM_GPT_35;
45
import static org.assertj.core.api.Assertions.assertThat;
56
import static org.assertj.core.api.Assertions.assertThatThrownBy;
67

78
import com.sap.ai.sdk.orchestration.client.model.ChatMessage;
9+
import com.sap.ai.sdk.orchestration.client.model.DPIConfig;
10+
import com.sap.ai.sdk.orchestration.client.model.DPIEntities;
811
import com.sap.ai.sdk.orchestration.client.model.Template;
912
import java.util.List;
1013
import java.util.Map;
@@ -75,4 +78,50 @@ void testMessagesHistory() {
7578

7679
assertThat(actual.getMessagesHistory()).containsExactly(systemMessage);
7780
}
81+
82+
@Test
83+
void testDpiMaskingConfig() {
84+
var maskingConfig = DpiMasking.anonymization().withEntities(DPIEntities.ADDRESS);
85+
var config =
86+
new OrchestrationModuleConfig()
87+
.withLlmConfig(CUSTOM_GPT_35)
88+
.withMaskingConfig(maskingConfig);
89+
90+
var actual = ConfigToRequestTransformer.toModuleConfigs(config);
91+
92+
assertThat(actual.getMaskingModuleConfig()).isNotNull();
93+
assertThat(actual.getMaskingModuleConfig().getMaskingProviders()).hasSize(1);
94+
DPIConfig dpiConfig = (DPIConfig) actual.getMaskingModuleConfig().getMaskingProviders().get(0);
95+
assertThat(dpiConfig.getMethod()).isEqualTo(DPIConfig.MethodEnum.ANONYMIZATION);
96+
assertThat(dpiConfig.getEntities()).hasSize(1);
97+
assertThat(dpiConfig.getEntities().get(0).getType()).isEqualTo(DPIEntities.ADDRESS);
98+
99+
var configModified = config.withMaskingConfig(maskingConfig);
100+
assertThat(configModified.getMaskingConfig()).isNotNull();
101+
assertThat(configModified.getMaskingConfig().getMaskingProviders())
102+
.withFailMessage("withMaskingConfig() should overwrite the existing config and not append")
103+
.hasSize(1);
104+
}
105+
106+
@Test
107+
void testLLMConfig() {
108+
Map<String, Object> params = Map.of("foo", "bar");
109+
String version = "2024-05-13";
110+
OrchestrationAiModel aiModel = GPT_4O.withModelParams(params).withModelVersion(version);
111+
var config = new OrchestrationModuleConfig().withLlmConfig(aiModel);
112+
113+
var actual = ConfigToRequestTransformer.toModuleConfigs(config);
114+
115+
assertThat(actual.getLlmModuleConfig()).isNotNull();
116+
assertThat(actual.getLlmModuleConfig().getModelName()).isEqualTo(GPT_4O.getModelName());
117+
assertThat(actual.getLlmModuleConfig().getModelParams()).isEqualTo(params);
118+
assertThat(actual.getLlmModuleConfig().getModelVersion()).isEqualTo(version);
119+
120+
assertThat(GPT_4O.getModelParams())
121+
.withFailMessage("Static models should be unchanged")
122+
.isEmpty();
123+
assertThat(GPT_4O.getModelVersion())
124+
.withFailMessage("Static models should be unchanged")
125+
.isEqualTo("latest");
126+
}
78127
}

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

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,15 @@
3434
import com.sap.ai.sdk.orchestration.client.model.AzureThreshold;
3535
import com.sap.ai.sdk.orchestration.client.model.ChatMessage;
3636
import com.sap.ai.sdk.orchestration.client.model.CompletionPostRequest;
37-
import com.sap.ai.sdk.orchestration.client.model.DPIConfig;
3837
import com.sap.ai.sdk.orchestration.client.model.DPIEntities;
39-
import com.sap.ai.sdk.orchestration.client.model.DPIEntityConfig;
4038
import com.sap.ai.sdk.orchestration.client.model.FilteringModuleConfig;
4139
import com.sap.ai.sdk.orchestration.client.model.GenericModuleResult;
4240
import com.sap.ai.sdk.orchestration.client.model.InputFilteringConfig;
4341
import com.sap.ai.sdk.orchestration.client.model.LLMModuleResultSynchronous;
44-
import com.sap.ai.sdk.orchestration.client.model.MaskingModuleConfig;
4542
import com.sap.ai.sdk.orchestration.client.model.OutputFilteringConfig;
4643
import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination;
4744
import java.io.IOException;
4845
import java.io.InputStream;
49-
import java.util.Arrays;
5046
import java.util.List;
5147
import java.util.Map;
5248
import java.util.Objects;
@@ -306,8 +302,7 @@ void maskingPseudonymization() throws IOException {
306302
.withBodyFile("maskingResponse.json")
307303
.withHeader("Content-Type", "application/json")));
308304

309-
final var maskingConfig =
310-
createMaskingConfig(DPIConfig.MethodEnum.PSEUDONYMIZATION, DPIEntities.PHONE);
305+
final var maskingConfig = DpiMasking.pseudonymization().withEntities(DPIEntities.PHONE);
311306

312307
final var result = client.chatCompletion(prompt, config.withMaskingConfig(maskingConfig));
313308
final var response = result.getOriginalResponse();
@@ -328,20 +323,6 @@ void maskingPseudonymization() throws IOException {
328323
}
329324
}
330325

331-
private static MaskingModuleConfig createMaskingConfig(
332-
@Nonnull final DPIConfig.MethodEnum method, @Nonnull final DPIEntities... entities) {
333-
334-
final var entityConfigs =
335-
Arrays.stream(entities).map(it -> new DPIEntityConfig().type(it)).toList();
336-
return new MaskingModuleConfig()
337-
.maskingProviders(
338-
List.of(
339-
new DPIConfig()
340-
.type(DPIConfig.TypeEnum.SAP_DATA_PRIVACY_INTEGRATION)
341-
.method(method)
342-
.entities(entityConfigs)));
343-
}
344-
345326
@Test
346327
void testErrorHandling() {
347328
stubFor(

pom.xml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,11 @@
7676
<enforcer.skipEnforceScopeLombok>false</enforcer.skipEnforceScopeLombok>
7777
<enforcer.skipBanGeneratedModulesReference>false</enforcer.skipBanGeneratedModulesReference>
7878
<!-- Test coverage -->
79-
<coverage.instruction>74%</coverage.instruction>
79+
<coverage.instruction>75%</coverage.instruction>
8080
<coverage.branch>67%</coverage.branch>
81-
<coverage.complexity>67%</coverage.complexity>
82-
<coverage.line>75%</coverage.line>
83-
<coverage.method>80%</coverage.method>
81+
<coverage.complexity>69%</coverage.complexity>
82+
<coverage.line>76%</coverage.line>
83+
<coverage.method>85%</coverage.method>
8484
<coverage.class>85%</coverage.class>
8585
</properties>
8686

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

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.GPT_35_TURBO;
44

5+
import com.sap.ai.sdk.orchestration.DpiMasking;
56
import com.sap.ai.sdk.orchestration.OrchestrationChatResponse;
67
import com.sap.ai.sdk.orchestration.OrchestrationClient;
78
import com.sap.ai.sdk.orchestration.OrchestrationModuleConfig;
@@ -10,15 +11,11 @@
1011
import com.sap.ai.sdk.orchestration.client.model.AzureContentSafetyFilterConfig;
1112
import com.sap.ai.sdk.orchestration.client.model.AzureThreshold;
1213
import com.sap.ai.sdk.orchestration.client.model.ChatMessage;
13-
import com.sap.ai.sdk.orchestration.client.model.DPIConfig;
1414
import com.sap.ai.sdk.orchestration.client.model.DPIEntities;
15-
import com.sap.ai.sdk.orchestration.client.model.DPIEntityConfig;
1615
import com.sap.ai.sdk.orchestration.client.model.FilteringModuleConfig;
1716
import com.sap.ai.sdk.orchestration.client.model.InputFilteringConfig;
18-
import com.sap.ai.sdk.orchestration.client.model.MaskingModuleConfig;
1917
import com.sap.ai.sdk.orchestration.client.model.OutputFilteringConfig;
2018
import com.sap.ai.sdk.orchestration.client.model.Template;
21-
import java.util.Arrays;
2219
import java.util.List;
2320
import java.util.Map;
2421
import javax.annotation.Nonnull;
@@ -158,8 +155,7 @@ public OrchestrationChatResponse maskingAnonymization() {
158155
""");
159156

160157
final var prompt = new OrchestrationPrompt(systemMessage, userMessage);
161-
final var maskingConfig =
162-
createMaskingConfig(DPIConfig.MethodEnum.ANONYMIZATION, DPIEntities.PERSON);
158+
final var maskingConfig = DpiMasking.anonymization().withEntities(DPIEntities.PERSON);
163159
final var configWithMasking = config.withMaskingConfig(maskingConfig);
164160

165161
return client.chatCompletion(prompt, configWithMasking);
@@ -197,31 +193,9 @@ public OrchestrationChatResponse maskingPseudonymization() {
197193

198194
final var prompt = new OrchestrationPrompt(systemMessage, userMessage);
199195
final var maskingConfig =
200-
createMaskingConfig(
201-
DPIConfig.MethodEnum.PSEUDONYMIZATION, DPIEntities.PERSON, DPIEntities.EMAIL);
196+
DpiMasking.pseudonymization().withEntities(DPIEntities.PERSON, DPIEntities.EMAIL);
202197
final var configWithMasking = config.withMaskingConfig(maskingConfig);
203198

204199
return client.chatCompletion(prompt, configWithMasking);
205200
}
206-
207-
/**
208-
* Helper method to build masking configurations.
209-
*
210-
* @param method Either anonymization or pseudonymization.
211-
* @param entities The entities to mask.
212-
* @return A new masking configuration object.
213-
*/
214-
private static MaskingModuleConfig createMaskingConfig(
215-
@Nonnull final DPIConfig.MethodEnum method, @Nonnull final DPIEntities... entities) {
216-
217-
final var entityConfigs =
218-
Arrays.stream(entities).map(it -> new DPIEntityConfig().type(it)).toList();
219-
return new MaskingModuleConfig()
220-
.maskingProviders(
221-
List.of(
222-
new DPIConfig()
223-
.type(DPIConfig.TypeEnum.SAP_DATA_PRIVACY_INTEGRATION)
224-
.method(method)
225-
.entities(entityConfigs)));
226-
}
227201
}

0 commit comments

Comments
 (0)