Skip to content

Commit d4f8048

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/orch-convenient-filtering
# Conflicts: # orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java # orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java # sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java
2 parents 0b8112c + 5e57895 commit d4f8048

File tree

9 files changed

+185
-64
lines changed

9 files changed

+185
-64
lines changed

.github/workflows/e2e-test.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: "End-to-end Tests"
22
on:
33
workflow_dispatch:
44
schedule:
5-
- cron: 0 22 * * *
5+
- cron: 0 2 * * *
66

77
env:
88
MVN_MULTI_THREADED_ARGS: --batch-mode --no-transfer-progress --fail-at-end --show-version --threads 1C

docs/guides/ORCHESTRATION_CHAT_COMPLETION.md

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

173173
```java
174-
var maskingProvider =
175-
MaskingProviderConfig.create()
176-
.type(MaskingProviderConfig.TypeEnum.SAP_DATA_PRIVACY_INTEGRATION)
177-
.method(MaskingProviderConfig.MethodEnum.ANONYMIZATION)
178-
.entities(
179-
DPIEntityConfig.create().type(DPIEntities.PHONE),
180-
DPIEntityConfig.create().type(DPIEntities.PERSON));
181-
var maskingConfig = MaskingModuleConfig.create().maskingProviders(maskingProvider);
174+
var maskingConfig =
175+
DpiMasking.anonymization().withEntities(DPIEntities.PHONE, DPIEntities.PERSON);
182176
var configWithMasking = config.withMaskingConfig(maskingConfig);
183177

184178
var systemMessage = ChatMessage.create()
@@ -197,7 +191,7 @@ var result =
197191
new OrchestrationClient().chatCompletion(prompt, configWithMasking);
198192
```
199193

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

202196
### Set model parameters
203197

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: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import lombok.Value;
1616
import lombok.With;
1717
import lombok.experimental.Tolerate;
18+
import lombok.val;
1819

1920
/**
2021
* Represents the configuration for the orchestration service. Allows for configuring the different
@@ -66,6 +67,26 @@ public OrchestrationModuleConfig withLlmConfig(@Nonnull final OrchestrationAiMod
6667
return withLlmConfig(aiModel.createConfig());
6768
}
6869

70+
/**
71+
* Creates a new configuration with the given Data Masking configuration.
72+
*
73+
* @param maskingProvider The Data Masking configuration to use.
74+
* @param maskingProviders Additional Data Masking configurations to use.
75+
* @return A new configuration with the given Data Masking configuration.
76+
*/
77+
@Tolerate
78+
@Nonnull
79+
public OrchestrationModuleConfig withMaskingConfig(
80+
@Nonnull final MaskingProvider maskingProvider,
81+
@Nonnull final MaskingProvider... maskingProviders) {
82+
val newMaskingConfig =
83+
new MaskingModuleConfig().addMaskingProvidersItem(maskingProvider.createConfig());
84+
Arrays.stream(maskingProviders)
85+
.forEach(it -> newMaskingConfig.addMaskingProvidersItem(it.createConfig()));
86+
87+
return withMaskingConfig(newMaskingConfig);
88+
}
89+
6990
/**
7091
* Adds input content filters to the orchestration configuration.
7192
*

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 & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,21 +31,16 @@
3131
import com.sap.ai.sdk.core.AiCoreService;
3232
import com.sap.ai.sdk.orchestration.client.model.ChatMessage;
3333
import com.sap.ai.sdk.orchestration.client.model.CompletionPostRequest;
34-
import com.sap.ai.sdk.orchestration.client.model.DPIConfig;
3534
import com.sap.ai.sdk.orchestration.client.model.DPIEntities;
36-
import com.sap.ai.sdk.orchestration.client.model.DPIEntityConfig;
3735
import com.sap.ai.sdk.orchestration.client.model.GenericModuleResult;
3836
import com.sap.ai.sdk.orchestration.client.model.LLMModuleResultSynchronous;
39-
import com.sap.ai.sdk.orchestration.client.model.MaskingModuleConfig;
4037
import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination;
4138
import java.io.IOException;
4239
import java.io.InputStream;
43-
import java.util.Arrays;
4440
import java.util.List;
4541
import java.util.Map;
4642
import java.util.Objects;
4743
import java.util.function.Function;
48-
import javax.annotation.Nonnull;
4944
import org.assertj.core.api.SoftAssertions;
5045
import org.junit.jupiter.api.BeforeEach;
5146
import org.junit.jupiter.api.Test;
@@ -293,8 +288,7 @@ void maskingPseudonymization() throws IOException {
293288
.withBodyFile("maskingResponse.json")
294289
.withHeader("Content-Type", "application/json")));
295290

296-
final var maskingConfig =
297-
createMaskingConfig(DPIConfig.MethodEnum.PSEUDONYMIZATION, DPIEntities.PHONE);
291+
final var maskingConfig = DpiMasking.pseudonymization().withEntities(DPIEntities.PHONE);
298292

299293
final var result = client.chatCompletion(prompt, config.withMaskingConfig(maskingConfig));
300294
final var response = result.getOriginalResponse();
@@ -315,20 +309,6 @@ void maskingPseudonymization() throws IOException {
315309
}
316310
}
317311

318-
private static MaskingModuleConfig createMaskingConfig(
319-
@Nonnull final DPIConfig.MethodEnum method, @Nonnull final DPIEntities... entities) {
320-
321-
final var entityConfigs =
322-
Arrays.stream(entities).map(it -> new DPIEntityConfig().type(it)).toList();
323-
return new MaskingModuleConfig()
324-
.maskingProviders(
325-
List.of(
326-
new DPIConfig()
327-
.type(DPIConfig.TypeEnum.SAP_DATA_PRIVACY_INTEGRATION)
328-
.method(method)
329-
.entities(entityConfigs)));
330-
}
331-
332312
@Test
333313
void testErrorHandling() {
334314
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
@@ -4,17 +4,14 @@
44

55
import com.sap.ai.sdk.orchestration.AzureContentFilter;
66
import com.sap.ai.sdk.orchestration.AzureModerationPolicy;
7+
import com.sap.ai.sdk.orchestration.DpiMasking;
78
import com.sap.ai.sdk.orchestration.OrchestrationChatResponse;
89
import com.sap.ai.sdk.orchestration.OrchestrationClient;
910
import com.sap.ai.sdk.orchestration.OrchestrationModuleConfig;
1011
import com.sap.ai.sdk.orchestration.OrchestrationPrompt;
1112
import com.sap.ai.sdk.orchestration.client.model.ChatMessage;
12-
import com.sap.ai.sdk.orchestration.client.model.DPIConfig;
1313
import com.sap.ai.sdk.orchestration.client.model.DPIEntities;
14-
import com.sap.ai.sdk.orchestration.client.model.DPIEntityConfig;
15-
import com.sap.ai.sdk.orchestration.client.model.MaskingModuleConfig;
1614
import com.sap.ai.sdk.orchestration.client.model.Template;
17-
import java.util.Arrays;
1815
import java.util.List;
1916
import java.util.Map;
2017
import javax.annotation.Nonnull;
@@ -134,8 +131,7 @@ public OrchestrationChatResponse maskingAnonymization() {
134131
""");
135132

136133
final var prompt = new OrchestrationPrompt(systemMessage, userMessage);
137-
final var maskingConfig =
138-
createMaskingConfig(DPIConfig.MethodEnum.ANONYMIZATION, DPIEntities.PERSON);
134+
final var maskingConfig = DpiMasking.anonymization().withEntities(DPIEntities.PERSON);
139135
final var configWithMasking = config.withMaskingConfig(maskingConfig);
140136

141137
return client.chatCompletion(prompt, configWithMasking);
@@ -173,31 +169,9 @@ public OrchestrationChatResponse maskingPseudonymization() {
173169

174170
final var prompt = new OrchestrationPrompt(systemMessage, userMessage);
175171
final var maskingConfig =
176-
createMaskingConfig(
177-
DPIConfig.MethodEnum.PSEUDONYMIZATION, DPIEntities.PERSON, DPIEntities.EMAIL);
172+
DpiMasking.pseudonymization().withEntities(DPIEntities.PERSON, DPIEntities.EMAIL);
178173
final var configWithMasking = config.withMaskingConfig(maskingConfig);
179174

180175
return client.chatCompletion(prompt, configWithMasking);
181176
}
182-
183-
/**
184-
* Helper method to build masking configurations.
185-
*
186-
* @param method Either anonymization or pseudonymization.
187-
* @param entities The entities to mask.
188-
* @return A new masking configuration object.
189-
*/
190-
private static MaskingModuleConfig createMaskingConfig(
191-
@Nonnull final DPIConfig.MethodEnum method, @Nonnull final DPIEntities... entities) {
192-
193-
final var entityConfigs =
194-
Arrays.stream(entities).map(it -> new DPIEntityConfig().type(it)).toList();
195-
return new MaskingModuleConfig()
196-
.maskingProviders(
197-
List.of(
198-
new DPIConfig()
199-
.type(DPIConfig.TypeEnum.SAP_DATA_PRIVACY_INTEGRATION)
200-
.method(method)
201-
.entities(entityConfigs)));
202-
}
203177
}

0 commit comments

Comments
 (0)