Skip to content

Commit fce3bd2

Browse files
n-o-u-r-h-a-nbot-sdk-jsJonas-Isrnewtork
authored
feat: support using RG scoped prompt templates (SAP#722)
* Updating TemplateConfig.java. * rolling back * Orchestration Convenience RG-Scoped Prompt Templates * Triggering * Triggering 2 * Rolling back renaming avoiding API breakage. * Formatting * Changing RG id + minor format * Changing API Approach * Formatting * Adding JavaDocs + Formatting * Removing LLM with Image Support * Updating release notes * Making exception error message clearer * Making exception error message clearer * Update orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClientException.java Co-authored-by: Alexander Dümont <alexander.duemont@sap.com> * Update docs/release_notes.md Co-authored-by: Alexander Dümont <alexander.duemont@sap.com> * Formatting * updating javadoc * fixing error message * reverting coverage checks --------- Co-authored-by: SAP Cloud SDK Bot <cloudsdk@sap.com> Co-authored-by: Jonas-Isr <jonas.israel@sap.com> Co-authored-by: Alexander Dümont <alexander.duemont@sap.com>
1 parent a70e89e commit fce3bd2

13 files changed

Lines changed: 453 additions & 29 deletions

File tree

docs/release_notes.md

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

1717
### 📈 Improvements
1818

19-
-
19+
-[Orchestration] Added new API `OrchestrationTemplateReference#withScope` to support prompt templates with resource-group scope.
2020

2121
### 🐛 Fixed Issues
2222

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ public class OrchestrationClientException extends ClientException {
2929
(message, clientError, cause) -> {
3030
final var details = extractInputFilterDetails(clientError);
3131
if (details.isEmpty()) {
32+
if (message.contains("No Prompt Template found in the Prompt Registry.")) {
33+
message +=
34+
"\n Please make sure to provide a resource group id and verify that it matches the provided template reference details if the template is referenced from a resource-group scope, otherwise use the tenant scope without providing resource group id.";
35+
}
3236
return new OrchestrationClientException(message, cause).setClientError(clientError);
3337
}
3438
return new Input(message, cause).setFilterDetails(details).setClientError(clientError);

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
import com.google.common.annotations.Beta;
44
import com.sap.ai.sdk.orchestration.model.PromptTemplatingModuleConfigPrompt;
55
import com.sap.ai.sdk.orchestration.model.TemplateRef;
6+
import com.sap.ai.sdk.orchestration.model.TemplateRefByID;
7+
import com.sap.ai.sdk.orchestration.model.TemplateRefByScenarioNameVersion;
68
import com.sap.ai.sdk.orchestration.model.TemplateRefTemplateRef;
79
import javax.annotation.Nonnull;
810
import lombok.AccessLevel;
911
import lombok.AllArgsConstructor;
1012
import lombok.EqualsAndHashCode;
1113
import lombok.Value;
14+
import lombok.With;
1215

1316
/**
1417
* A reference to a template to use in {@link OrchestrationModuleConfig}.
@@ -22,6 +25,9 @@
2225
public class OrchestrationTemplateReference extends TemplateConfig {
2326
@Nonnull TemplateRefTemplateRef reference;
2427

28+
/** The scope of the template reference. */
29+
@With @Nonnull ScopeEnum scope;
30+
2531
/**
2632
* Create a low-level representation of the template.
2733
*
@@ -30,6 +36,25 @@ public class OrchestrationTemplateReference extends TemplateConfig {
3036
@Nonnull
3137
@Override
3238
protected PromptTemplatingModuleConfigPrompt toLowLevel() {
33-
return TemplateRef.create().templateRef(reference);
39+
if (reference instanceof TemplateRefByID idRef) {
40+
final var valueById = TemplateRefByID.ScopeEnum.valueOf(scope.name());
41+
idRef.setScope(valueById);
42+
return TemplateRef.create().templateRef(idRef);
43+
} else if (reference instanceof TemplateRefByScenarioNameVersion scenarioRef) {
44+
final var valueByScenario = TemplateRefByScenarioNameVersion.ScopeEnum.valueOf(scope.name());
45+
scenarioRef.setScope(valueByScenario);
46+
return TemplateRef.create().templateRef(scenarioRef);
47+
} else {
48+
throw new IllegalStateException(
49+
"Unsupported template reference type: " + reference.getClass());
50+
}
51+
}
52+
53+
/** Enum representing the scope of the template reference. */
54+
public enum ScopeEnum {
55+
/** Template is resolved within the current tenant scope. */
56+
TENANT,
57+
/** Template is resolved within the configured resource group scope. */
58+
RESOURCE_GROUP
3459
}
3560
}

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

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.sap.ai.sdk.orchestration;
22

3+
import static com.sap.ai.sdk.orchestration.OrchestrationTemplateReference.ScopeEnum.TENANT;
4+
35
import com.google.common.annotations.Beta;
46
import com.sap.ai.sdk.orchestration.model.PromptTemplatingModuleConfigPrompt;
57
import com.sap.ai.sdk.orchestration.model.TemplateRefByID;
@@ -35,28 +37,33 @@ public static OrchestrationTemplate create() {
3537
}
3638

3739
/**
38-
* Build a template reference.
40+
* Build a template reference with tenant level scope.
3941
*
4042
* @return An intermediate object to build the template reference.
4143
*/
4244
@Nonnull
4345
public static ReferenceBuilder reference() {
4446
final var templ = TemplateRefByScenarioNameVersion.create();
45-
return s -> n -> v -> new OrchestrationTemplateReference(templ.scenario(s).name(n).version(v));
47+
48+
return scenario ->
49+
name ->
50+
version ->
51+
new OrchestrationTemplateReference(
52+
templ.scenario(scenario).name(name).version(version), TENANT);
4653
}
4754

4855
/** Intermediate object to build a template reference. */
4956
@FunctionalInterface
5057
public interface ReferenceBuilder {
5158
/**
52-
* Build a template reference with the given id.
59+
* Build a template reference with the given id for tenant scope.
5360
*
5461
* @param id The id of the template.
5562
* @return A template reference with the given id.
5663
*/
5764
@Nonnull
5865
default OrchestrationTemplateReference byId(@Nonnull final String id) {
59-
return new OrchestrationTemplateReference(TemplateRefByID.create().id(id));
66+
return new OrchestrationTemplateReference(TemplateRefByID.create().id(id), TENANT);
6067
}
6168

6269
/**

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

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.sap.ai.sdk.orchestration;
22

3+
import static com.sap.ai.sdk.orchestration.OrchestrationTemplateReference.ScopeEnum.RESOURCE_GROUP;
4+
import static com.sap.ai.sdk.orchestration.OrchestrationTemplateReference.ScopeEnum.TENANT;
35
import static com.sap.ai.sdk.orchestration.model.UserChatMessage.RoleEnum.USER;
46
import static org.assertj.core.api.Assertions.assertThat;
57

@@ -160,20 +162,35 @@ void testTemplateConstruction() {
160162
void testTemplateReferenceConstruction() {
161163
var templateReferenceId = TemplateConfig.reference().byId("id");
162164
var expectedTemplateReferenceId =
163-
new OrchestrationTemplateReference(TemplateRefByID.create().id("id"));
165+
new OrchestrationTemplateReference(TemplateRefByID.create().id("id"), TENANT);
164166
var templateReferenceIdLowLevel =
165167
TemplateRef.create().templateRef(TemplateRefByID.create().id("id"));
166168
assertThat(templateReferenceId).isEqualTo(expectedTemplateReferenceId);
167169
assertThat(templateReferenceId.toLowLevel()).isEqualTo(templateReferenceIdLowLevel);
168170

171+
templateReferenceId = TemplateConfig.reference().byId("id").withScope(RESOURCE_GROUP);
172+
expectedTemplateReferenceId =
173+
new OrchestrationTemplateReference(TemplateRefByID.create().id("id"), RESOURCE_GROUP);
174+
templateReferenceIdLowLevel =
175+
TemplateRef.create()
176+
.templateRef(
177+
TemplateRefByID.create().id("id").scope(TemplateRefByID.ScopeEnum.RESOURCE_GROUP));
178+
assertThat(templateReferenceId).isEqualTo(expectedTemplateReferenceId);
179+
assertThat(templateReferenceId.toLowLevel()).isEqualTo(templateReferenceIdLowLevel);
180+
169181
var templateReferenceScenarioNameVersion =
170-
TemplateConfig.reference().byScenario("scenario").name("name").version("version");
182+
TemplateConfig.reference()
183+
.byScenario("scenario")
184+
.name("name")
185+
.version("version")
186+
.withScope(TENANT);
171187
var expectedTemplateReferenceScenarioNameVersion =
172188
new OrchestrationTemplateReference(
173189
TemplateRefByScenarioNameVersion.create()
174190
.scenario("scenario")
175191
.name("name")
176-
.version("version"));
192+
.version("version"),
193+
TENANT);
177194
var templateReferenceScenarioNameVersionLowLevel =
178195
TemplateRef.create()
179196
.templateRef(
@@ -185,6 +202,33 @@ void testTemplateReferenceConstruction() {
185202
.isEqualTo(expectedTemplateReferenceScenarioNameVersion);
186203
assertThat(templateReferenceScenarioNameVersion.toLowLevel())
187204
.isEqualTo(templateReferenceScenarioNameVersionLowLevel);
205+
206+
templateReferenceScenarioNameVersion =
207+
TemplateConfig.reference()
208+
.byScenario("scenario")
209+
.name("name")
210+
.version("version")
211+
.withScope(RESOURCE_GROUP);
212+
var scopeScenario = TemplateRefByScenarioNameVersion.ScopeEnum.RESOURCE_GROUP;
213+
expectedTemplateReferenceScenarioNameVersion =
214+
new OrchestrationTemplateReference(
215+
TemplateRefByScenarioNameVersion.create()
216+
.scenario("scenario")
217+
.name("name")
218+
.version("version"),
219+
RESOURCE_GROUP);
220+
templateReferenceScenarioNameVersionLowLevel =
221+
TemplateRef.create()
222+
.templateRef(
223+
TemplateRefByScenarioNameVersion.create()
224+
.scenario("scenario")
225+
.name("name")
226+
.version("version")
227+
.scope(scopeScenario));
228+
assertThat(templateReferenceScenarioNameVersion)
229+
.isEqualTo(expectedTemplateReferenceScenarioNameVersion);
230+
assertThat(templateReferenceScenarioNameVersion.toLowLevel())
231+
.isEqualTo(templateReferenceScenarioNameVersionLowLevel);
188232
}
189233

190234
@Test

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

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
1818
import static com.sap.ai.sdk.orchestration.AzureFilterThreshold.ALLOW_SAFE;
1919
import static com.sap.ai.sdk.orchestration.AzureFilterThreshold.ALLOW_SAFE_LOW_MEDIUM;
20+
import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.GEMINI_2_5_FLASH;
2021
import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.GPT_4O;
2122
import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.GPT_4O_MINI;
2223
import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.Parameter.*;
24+
import static com.sap.ai.sdk.orchestration.OrchestrationTemplateReference.ScopeEnum.RESOURCE_GROUP;
2325
import static com.sap.ai.sdk.orchestration.model.AzureThreshold.NUMBER_0;
2426
import static com.sap.ai.sdk.orchestration.model.AzureThreshold.NUMBER_4;
2527
import static com.sap.ai.sdk.orchestration.model.AzureThreshold.NUMBER_6;
@@ -1259,7 +1261,7 @@ void testResponseFormatText() throws IOException {
12591261
}
12601262

12611263
@Test
1262-
void testTemplateFromPromptRegistryById() throws IOException {
1264+
void testTemplateFromPromptRegistryByIdTenant() throws IOException {
12631265
{
12641266
stubFor(
12651267
post(anyUrl())
@@ -1285,7 +1287,44 @@ void testTemplateFromPromptRegistryById() throws IOException {
12851287
}
12861288

12871289
@Test
1288-
void testTemplateFromPromptRegistryByScenario() throws IOException {
1290+
void testTemplateFromPromptRegistryByIdResourceGroup() throws IOException {
1291+
{
1292+
stubFor(
1293+
post(anyUrl())
1294+
.willReturn(
1295+
aResponse()
1296+
.withBodyFile("templateReferenceResourceGroupResponse.json")
1297+
.withHeader("Content-Type", "application/json")));
1298+
1299+
var template =
1300+
TemplateConfig.reference()
1301+
.byId("8bf72116-11ab-41bb-8933-8be56f59cb67")
1302+
.withScope(RESOURCE_GROUP);
1303+
var config =
1304+
new OrchestrationModuleConfig()
1305+
.withLlmConfig(GEMINI_2_5_FLASH.withParam(TEMPERATURE, 0.0));
1306+
var configWithTemplate = config.withTemplateConfig(template);
1307+
1308+
var inputParams =
1309+
Map.of(
1310+
"categories",
1311+
"Finance, Tech, Sports",
1312+
"inputExample",
1313+
"What's the latest news on the stock market?");
1314+
var prompt = new OrchestrationPrompt(inputParams);
1315+
1316+
final var response = client.chatCompletion(prompt, configWithTemplate);
1317+
assertThat(response.getContent()).startsWith("Finance");
1318+
assertThat(response.getOriginalResponse().getIntermediateResults().getTemplating())
1319+
.hasSize(2);
1320+
1321+
final String request = fileLoaderStr.apply("templateReferenceResourceGroupByIdRequest.json");
1322+
verify(postRequestedFor(anyUrl()).withRequestBody(equalToJson(request)));
1323+
}
1324+
}
1325+
1326+
@Test
1327+
void testTemplateFromPromptRegistryByScenarioTenant() throws IOException {
12891328
stubFor(
12901329
post(anyUrl())
12911330
.willReturn(
@@ -1307,6 +1346,42 @@ void testTemplateFromPromptRegistryByScenario() throws IOException {
13071346
verify(postRequestedFor(anyUrl()).withRequestBody(equalToJson(request)));
13081347
}
13091348

1349+
@Test
1350+
void testTemplateFromPromptRegistryByScenarioResourceGroup() throws IOException {
1351+
stubFor(
1352+
post(anyUrl())
1353+
.willReturn(
1354+
aResponse()
1355+
.withBodyFile("templateReferenceResourceGroupResponse.json")
1356+
.withHeader("Content-Type", "application/json")));
1357+
1358+
var template =
1359+
TemplateConfig.reference()
1360+
.byScenario("categorization")
1361+
.name("example-prompt-template")
1362+
.version("0.0.1")
1363+
.withScope(RESOURCE_GROUP);
1364+
var config =
1365+
new OrchestrationModuleConfig().withLlmConfig(GEMINI_2_5_FLASH.withParam(TEMPERATURE, 0.0));
1366+
var configWithTemplate = config.withTemplateConfig(template);
1367+
1368+
var inputParams =
1369+
Map.of(
1370+
"categories",
1371+
"Finance, Tech, Sports",
1372+
"inputExample",
1373+
"What's the latest news on the stock market?");
1374+
var prompt = new OrchestrationPrompt(inputParams);
1375+
1376+
final var response = client.chatCompletion(prompt, configWithTemplate);
1377+
assertThat(response.getContent()).startsWith("Finance");
1378+
assertThat(response.getOriginalResponse().getIntermediateResults().getTemplating()).hasSize(2);
1379+
1380+
final String request =
1381+
fileLoaderStr.apply("templateReferenceResourceGroupByScenarioRequest.json");
1382+
verify(postRequestedFor(anyUrl()).withRequestBody(equalToJson(request)));
1383+
}
1384+
13101385
@Test
13111386
void testTemplateFromInput() throws IOException {
13121387
stubFor(
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{
2+
"request_id": "921f38b7-3434-9171-85df-27be1b7fca3c",
3+
"intermediate_results": {
4+
"templating": [
5+
{
6+
"role": "system",
7+
"content": "You classify input text into the two following categories: Finance, Tech, Sports"
8+
},
9+
{
10+
"content": "What's the latest news on the stock market?",
11+
"role": "user"
12+
}
13+
],
14+
"llm": {
15+
"id": "",
16+
"object": "chat.completion",
17+
"created": 1769424215,
18+
"model": "gemini-2.5-flash",
19+
"choices": [
20+
{
21+
"index": 0,
22+
"message": {
23+
"role": "assistant",
24+
"content": "Finance"
25+
},
26+
"finish_reason": "stop"
27+
}
28+
],
29+
"usage": {
30+
"completion_tokens": 154,
31+
"prompt_tokens": 26,
32+
"total_tokens": 180,
33+
"prompt_tokens_details": {
34+
"cached_tokens": 0
35+
},
36+
"completion_tokens_details": {
37+
"reasoning_tokens": 153
38+
}
39+
}
40+
}
41+
},
42+
"final_result": {
43+
"id": "",
44+
"object": "chat.completion",
45+
"created": 1769424215,
46+
"model": "gemini-2.5-flash",
47+
"choices": [
48+
{
49+
"index": 0,
50+
"message": {
51+
"role": "assistant",
52+
"content": "Finance"
53+
},
54+
"finish_reason": "stop"
55+
}
56+
],
57+
"usage": {
58+
"completion_tokens": 154,
59+
"prompt_tokens": 26,
60+
"total_tokens": 180,
61+
"prompt_tokens_details": {
62+
"cached_tokens": 0
63+
},
64+
"completion_tokens_details": {
65+
"reasoning_tokens": 153
66+
}
67+
}
68+
}
69+
}

0 commit comments

Comments
 (0)