Skip to content

Commit 5b7ac24

Browse files
authored
Merge branch 'main' into drop-model-prefix
2 parents f89a75c + 5e57895 commit 5b7ac24

File tree

14 files changed

+321
-69
lines changed

14 files changed

+321
-69
lines changed
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
name: Test SAP Cloud SDK Versions
2+
3+
on:
4+
workflow_dispatch:
5+
6+
env:
7+
MVN_MULTI_THREADED_ARGS: --batch-mode --no-transfer-progress --fail-at-end --show-version --threads 1C
8+
JAVA_VERSION: 17
9+
10+
jobs:
11+
fetch-dependency-versions:
12+
runs-on: ubuntu-latest
13+
outputs:
14+
versions: ${{ steps.fetch-versions.outputs.VERSIONS }}
15+
16+
steps:
17+
- name: Fetch versions from Maven Central
18+
id: fetch-versions
19+
run: |
20+
# Specify the dependency coordinates
21+
GROUP_ID="com.sap.cloud.sdk"
22+
ARTIFACT_ID="sdk-bom"
23+
24+
# Fetch available versions from Maven Central API
25+
response=$(curl -s "https://search.maven.org/solrsearch/select?q=g:%22${GROUP_ID}%22+AND+a:%22${ARTIFACT_ID}%22&rows=15&core=gav&wt=json")
26+
27+
# Extract and filter versions (e.g., to exclude snapshots or specific versions)
28+
versions=$(echo "$response" | jq -r '.response.docs[].v' | grep -v -E 'SNAPSHOT|alpha|beta' | sort -V)
29+
30+
# Convert the versions to a JSON array
31+
json_versions=$(echo "$versions" | jq -R . | jq -s . | tr -d '\n')
32+
33+
echo "JSON Versions: $json_versions"
34+
35+
# Output the versions as a string that can be used in the matrix
36+
echo "VERSIONS=${json_versions}" >> $GITHUB_OUTPUT
37+
38+
setup-environment:
39+
runs-on: ubuntu-latest
40+
outputs:
41+
cache-key: ${{ steps.cache-build.outputs.cache-key }}
42+
steps:
43+
- name: "Checkout repository"
44+
uses: actions/checkout@v4
45+
46+
- name: "Setup java"
47+
uses: actions/setup-java@v4
48+
with:
49+
distribution: "temurin"
50+
java-version: ${{ env.JAVA_VERSION }}
51+
cache: 'maven'
52+
53+
- name: "Cache build"
54+
id: cache-build
55+
uses: actions/cache@v3
56+
with:
57+
path: |
58+
~/.m2/repository
59+
target
60+
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
61+
restore-keys: |
62+
${{ runner.os }}-maven-
63+
64+
- name: "Build SDK"
65+
run: |
66+
MVN_ARGS="${{ env.MVN_MULTI_THREADED_ARGS }} clean install -DskipTests -DskipFormatting"
67+
mvn $MVN_ARGS
68+
69+
test-dependency-versions:
70+
needs: [ fetch-dependency-versions, setup-environment ]
71+
runs-on: ubuntu-latest
72+
strategy:
73+
max-parallel: 1
74+
fail-fast: false
75+
matrix:
76+
version: ${{ fromJson(needs.fetch-dependency-versions.outputs.versions) }}
77+
continue-on-error: true
78+
steps:
79+
- name: "Checkout repository"
80+
uses: actions/checkout@v4
81+
82+
- name: "Setup java"
83+
uses: actions/setup-java@v4
84+
with:
85+
distribution: "temurin"
86+
java-version: ${{ env.JAVA_VERSION }}
87+
cache: 'maven'
88+
89+
- name: "Restore build cache"
90+
uses: actions/cache@v3
91+
with:
92+
path: |
93+
~/.m2/repository
94+
target
95+
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
96+
97+
- name: "Run tests with explicit version"
98+
run: |
99+
MVN_ARGS="${{ env.MVN_MULTI_THREADED_ARGS }} clean package -pl :spring-app -DskipTests=false -DskipFormatting -Dcloud-sdk.version=${{ matrix.version }} -Denforcer.skip=true -Dspotless.skip=true"
100+
mvn $MVN_ARGS
101+
env:
102+
# See "End-to-end test application instructions" on the README.md to update the secret
103+
AICORE_SERVICE_KEY: ${{ secrets.AICORE_SERVICE_KEY }}
104+
105+
- name: "Start Application Locally"
106+
run: |
107+
cd sample-code/spring-app
108+
mvn spring-boot:run &
109+
timeout=15
110+
while ! nc -z localhost 8080; do
111+
sleep 1
112+
timeout=$((timeout - 1))
113+
if [ $timeout -le 0 ]; then
114+
echo "Server did not start within 15 seconds."
115+
exit 1
116+
fi
117+
done
118+
env:
119+
# See "End-to-end test application instructions" on the README.md to update the secret
120+
AICORE_SERVICE_KEY: ${{ secrets.AICORE_SERVICE_KEY }}
121+
122+
- name: "Health Check"
123+
# print response body with headers to stdout. q:body only O:print -:stdout S:headers
124+
run: wget -qO- -S localhost:8080

.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

core/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<parent>
55
<groupId>com.sap.ai.sdk</groupId>
66
<artifactId>sdk-parent</artifactId>
7-
<version>0.2.0-SNAPSHOT</version>
7+
<version>0.3.0-SNAPSHOT</version>
88
</parent>
99
<artifactId>core</artifactId>
1010
<name>AI Core client</name>

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

foundation-models/openai/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<parent>
55
<groupId>com.sap.ai.sdk</groupId>
66
<artifactId>sdk-parent</artifactId>
7-
<version>0.2.0-SNAPSHOT</version>
7+
<version>0.3.0-SNAPSHOT</version>
88
<relativePath>../../pom.xml</relativePath>
99
</parent>
1010
<groupId>com.sap.ai.sdk.foundationmodels</groupId>

orchestration/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<parent>
55
<groupId>com.sap.ai.sdk</groupId>
66
<artifactId>sdk-parent</artifactId>
7-
<version>0.2.0-SNAPSHOT</version>
7+
<version>0.3.0-SNAPSHOT</version>
88
</parent>
99
<artifactId>orchestration</artifactId>
1010
<name>Orchestration client</name>
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
}

0 commit comments

Comments
 (0)