diff --git a/docs/release_notes.md b/docs/release_notes.md
index 80e2325c3..9181ad711 100644
--- a/docs/release_notes.md
+++ b/docs/release_notes.md
@@ -16,7 +16,7 @@
### 📈 Improvements
--
+- [Orchestration] Added new API `TranslationConfig#applyToPlaceholders` and `TranslationConfig#applyToTemplateRoles` to support partial translation for a message.
### 🐛 Fixed Issues
diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/TranslationConfig.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/TranslationConfig.java
index 6210f6f7a..b5ca61b9e 100644
--- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/TranslationConfig.java
+++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/TranslationConfig.java
@@ -1,12 +1,21 @@
package com.sap.ai.sdk.orchestration;
+import static com.sap.ai.sdk.orchestration.model.SAPDocumentTranslationApplyToSelector.CategoryEnum.PLACEHOLDERS;
+import static com.sap.ai.sdk.orchestration.model.SAPDocumentTranslationApplyToSelector.CategoryEnum.TEMPLATE_ROLES;
+
+import com.sap.ai.sdk.orchestration.model.SAPDocumentTranslationApplyToSelector;
import com.sap.ai.sdk.orchestration.model.SAPDocumentTranslationInput;
import com.sap.ai.sdk.orchestration.model.SAPDocumentTranslationInputConfig;
import com.sap.ai.sdk.orchestration.model.SAPDocumentTranslationOutput;
import com.sap.ai.sdk.orchestration.model.SAPDocumentTranslationOutputConfig;
import com.sap.ai.sdk.orchestration.model.SAPDocumentTranslationOutputTargetLanguage;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Stream;
import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
import lombok.AccessLevel;
+import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import lombok.With;
@@ -21,23 +30,129 @@
* @since 1.14.0
*/
public interface TranslationConfig {
+ /**
+ * Supported values for {@code items[]} when {@code category=template_roles}.
+ *
+ *
These map to the roles used in prompt templates.
+ */
+ @RequiredArgsConstructor
+ enum TemplateRole {
+ /** Template role for user messages. */
+ USER("user"),
+
+ /** Template role for system messages. */
+ SYSTEM("system"),
+
+ /** Template role for assistant messages. */
+ ASSISTANT("assistant"),
+
+ /** Template role for developer messages. */
+ DEVELOPER("developer"),
+
+ /** Template role for tool messages. */
+ TOOL("tool");
+
+ @Getter private final String value;
+ }
+
/** Input configuration for translation. */
@Value
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
class Input implements TranslationConfig {
String targetLanguage;
- @With String sourceLanguage;
+ String sourceLanguage;
- Object ApplyTo; // Can be null
+ /**
+ * Optional selection(s) to translate. If empty or null, translation is applied to the whole
+ * message. If used, source language will be applied per selector.
+ */
+ @Nullable List applyTo;
@Nonnull
SAPDocumentTranslationInput createSAPDocumentTranslationInput() {
val translationType = SAPDocumentTranslationInput.TypeEnum.SAP_DOCUMENT_TRANSLATION;
- val conf =
- SAPDocumentTranslationInputConfig.create().targetLanguage(targetLanguage).applyTo(null);
+ final var conf =
+ SAPDocumentTranslationInputConfig.create()
+ .targetLanguage(targetLanguage)
+ .applyTo(applyTo);
+
+ if (applyTo == null || applyTo.isEmpty()) {
+ conf.sourceLanguage(sourceLanguage);
+ }
+
return SAPDocumentTranslationInput.create().type(translationType).config(conf);
}
+
+ /**
+ * Start an {@code apply_to} selector for placeholder names in {@code placeholder_values}.
+ *
+ * @param name The first placeholder name to translate.
+ * @param additionalNames Additional placeholder names to translate.
+ * @return A selector with {@code category=placeholders} and the given items.
+ */
+ @Nonnull
+ public Input applyToPlaceholders(
+ @Nonnull final String name, @Nonnull final String... additionalNames) {
+ final var selector =
+ SAPDocumentTranslationApplyToSelector.create()
+ .category(PLACEHOLDERS)
+ .items(Stream.concat(Stream.of(name), Stream.of(additionalNames)).toList());
+ return addApplyToSelector(selector);
+ }
+
+ /**
+ * Start an {@code apply_to} selector for prompt template message roles.
+ *
+ * @param role The first template role to translate.
+ * @param roles The template roles to translate.
+ * @return A selector with {@code category=template_roles} and the given items.
+ */
+ @Nonnull
+ public Input applyToTemplateRoles(
+ @Nonnull final TranslationConfig.TemplateRole role,
+ @Nonnull final TranslationConfig.TemplateRole... roles) {
+ final var roleStrings = new ArrayList(1 + roles.length);
+ roleStrings.add(role.getValue());
+ for (final var r : roles) {
+ if (r != null) {
+ roleStrings.add(r.getValue());
+ }
+ }
+
+ final var selector =
+ SAPDocumentTranslationApplyToSelector.create()
+ .category(TEMPLATE_ROLES)
+ .items(roleStrings);
+ return addApplyToSelector(selector);
+ }
+
+ /**
+ * Set the source language for this translation.
+ * Important Note: If no selectors are used, this applies to the whole message.
+ * If selectors are used, this applies to the most recently added selector.
+ *
+ * @param sourceLanguage The source language code
+ * @return A new Input with the given source language applied.
+ */
+ @Nonnull
+ public Input withSourceLanguage(@Nonnull final String sourceLanguage) {
+ if (applyTo != null) {
+ applyTo.get(applyTo.size() - 1).sourceLanguage(sourceLanguage);
+ }
+ return new Input(targetLanguage, sourceLanguage, applyTo);
+ }
+
+ private Input addApplyToSelector(
+ @Nonnull final SAPDocumentTranslationApplyToSelector selector) {
+ final var appended = new ArrayList();
+ if (applyTo != null && !applyTo.isEmpty()) {
+ appended.addAll(applyTo);
+ }
+ appended.add(selector);
+
+ return new TranslationConfig.Input(targetLanguage, sourceLanguage, appended);
+ }
}
/** Output configuration for translation. */
@@ -71,7 +186,6 @@ SAPDocumentTranslationOutput createSAPDocumentTranslationOutput() {
*/
@Nonnull
static TranslationConfig.Input translateInputTo(@Nonnull final String targetLanguage) {
-
return new TranslationConfig.Input(targetLanguage, null, null);
}
@@ -86,7 +200,6 @@ static TranslationConfig.Input translateInputTo(@Nonnull final String targetLang
*/
@Nonnull
static TranslationConfig.Output translateOutputTo(@Nonnull final String targetLanguage) {
-
return new TranslationConfig.Output(targetLanguage, null);
}
}
diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfigTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfigTest.java
index 6e321cd7c..9cde42daa 100644
--- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfigTest.java
+++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfigTest.java
@@ -3,6 +3,8 @@
import static com.sap.ai.sdk.orchestration.AzureFilterThreshold.ALLOW_SAFE_LOW_MEDIUM;
import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.GPT_4O;
import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.Parameter.MAX_TOKENS;
+import static com.sap.ai.sdk.orchestration.TranslationConfig.TemplateRole.ASSISTANT;
+import static com.sap.ai.sdk.orchestration.TranslationConfig.TemplateRole.USER;
import static com.sap.ai.sdk.orchestration.model.DataRepositoryType.VECTOR;
import static com.sap.ai.sdk.orchestration.model.GroundingModuleConfig.TypeEnum.DOCUMENT_GROUNDING_SERVICE;
import static org.assertj.core.api.Assertions.assertThat;
@@ -20,6 +22,7 @@
import com.sap.ai.sdk.orchestration.model.MaskingModuleConfigProviders;
import com.sap.ai.sdk.orchestration.model.ResponseFormatJsonObject;
import com.sap.ai.sdk.orchestration.model.ResponseFormatJsonSchema;
+import com.sap.ai.sdk.orchestration.model.SAPDocumentTranslationOutputTargetLanguage;
import com.sap.ai.sdk.orchestration.model.Template;
import com.sap.ai.sdk.orchestration.model.TemplateRef;
import com.sap.ai.sdk.orchestration.model.TemplateRefByID;
@@ -145,35 +148,85 @@ void testTranslationConfig() {
TranslationConfig.translateInputTo("en-US").withSourceLanguage("de-DE");
var outputTranslationConfig =
TranslationConfig.translateOutputTo("de-DE").withSourceLanguage("en-US");
- var config =
+
+ var sapInput = inputTranslationConfig.createSAPDocumentTranslationInput();
+ var sapOutput = outputTranslationConfig.createSAPDocumentTranslationOutput();
+
+ assertThat(sapInput.getConfig()).isNotNull();
+ assertThat(sapInput.getConfig().getTargetLanguage()).isEqualTo("en-US");
+ assertThat(sapInput.getConfig().getSourceLanguage())
+ .isEqualTo(inputTranslationConfig.getSourceLanguage());
+
+ assertThat(sapOutput.getConfig()).isNotNull();
+ assertThat(
+ ((SAPDocumentTranslationOutputTargetLanguage.InnerString)
+ sapOutput.getConfig().getTargetLanguage())
+ .value())
+ .isEqualTo("de-DE");
+ assertThat(sapOutput.getConfig().getSourceLanguage())
+ .isEqualTo(outputTranslationConfig.getSourceLanguage());
+ }
+
+ @Test
+ void testTranslationConfigApplyToSelectors() {
+ var inputTranslationConfig =
+ TranslationConfig.translateInputTo("en-US")
+ .applyToPlaceholders("exam_type", "topic")
+ .withSourceLanguage("de-DE")
+ .applyToTemplateRoles(USER, ASSISTANT)
+ .withSourceLanguage("en-US");
+
+ var sapInput = inputTranslationConfig.createSAPDocumentTranslationInput();
+ assertThat(sapInput.getConfig().getTargetLanguage()).isEqualTo("en-US");
+ assertThat(sapInput.getConfig().getSourceLanguage()).isNull();
+ assertThat(sapInput.getConfig().getApplyTo().get(0).getSourceLanguage()).isEqualTo("de-DE");
+ assertThat(sapInput.getConfig().getApplyTo().get(1).getSourceLanguage()).isEqualTo("en-US");
+
+ assertThat(sapInput.getConfig().getApplyTo()).hasSize(2);
+ assertThat(sapInput.getConfig().getApplyTo().get(0).getCategory().getValue())
+ .isEqualTo("placeholders");
+ assertThat(sapInput.getConfig().getApplyTo().get(0).getItems())
+ .containsExactly("exam_type", "topic");
+ assertThat(sapInput.getConfig().getApplyTo().get(1).getCategory().getValue())
+ .isEqualTo("template_roles");
+ assertThat(sapInput.getConfig().getApplyTo().get(1).getItems())
+ .containsExactly("user", "assistant");
+
+ // applyTo == null list
+ final var inputNull = TranslationConfig.translateInputTo("en-US").withSourceLanguage("de-DE");
+ final var sapNull = inputNull.createSAPDocumentTranslationInput();
+ assertThat(sapNull.getConfig().getTargetLanguage()).isEqualTo("en-US");
+ assertThat(sapNull.getConfig().getSourceLanguage()).isEqualTo("de-DE");
+ assertThat(sapNull.getConfig().getApplyTo()).isNull();
+ }
+
+ @Test
+ void testTranslationConfigViaModuleConfig() {
+ final var inputTranslation =
+ TranslationConfig.translateInputTo("en-US").withSourceLanguage("de-DE");
+ final var outputTranslation =
+ TranslationConfig.translateOutputTo("de-DE").withSourceLanguage("en-US");
+
+ final var config =
new OrchestrationModuleConfig()
.withLlmConfig(GPT_4O)
- .withInputTranslationConfig(inputTranslationConfig)
- .withOutputTranslationConfig(outputTranslationConfig);
+ .withInputTranslationConfig(inputTranslation)
+ .withOutputTranslationConfig(outputTranslation);
assertThat(config.getInputTranslationConfig()).isNotNull();
assertThat(config.getInputTranslationConfig().getConfig().getTargetLanguage())
.isEqualTo("en-US");
assertThat(config.getInputTranslationConfig().getConfig().getSourceLanguage())
- .isEqualTo(
- inputTranslationConfig
- .createSAPDocumentTranslationInput()
- .getConfig()
- .getSourceLanguage());
+ .isEqualTo("de-DE");
assertThat(config.getOutputTranslationConfig()).isNotNull();
- assertThat(config.getOutputTranslationConfig().getConfig().getTargetLanguage())
- .isEqualTo(
- outputTranslationConfig
- .createSAPDocumentTranslationOutput()
- .getConfig()
- .getTargetLanguage());
+ assertThat(
+ ((SAPDocumentTranslationOutputTargetLanguage.InnerString)
+ config.getOutputTranslationConfig().getConfig().getTargetLanguage())
+ .value())
+ .isEqualTo("de-DE");
assertThat(config.getOutputTranslationConfig().getConfig().getSourceLanguage())
- .isEqualTo(
- outputTranslationConfig
- .createSAPDocumentTranslationOutput()
- .getConfig()
- .getSourceLanguage());
+ .isEqualTo("en-US");
}
@Test
diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java
index bb521adf2..91f9b1ab2 100644
--- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java
+++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java
@@ -690,17 +690,30 @@ public OrchestrationChatResponse localPromptTemplate(@Nonnull final String promp
*/
@Nonnull
public OrchestrationChatResponse translation() {
- val prompt =
- new OrchestrationPrompt(
- "Quelle est la couleur de la tour Eiffel? Et en quelle langue tu me parles maintenant?");
+ val inputParams =
+ Map.of("exam_type", "Abitur", "topic", "Deutsche Literatur", "num_questions", "5");
+
+ val systemMessage =
+ Message.system(
+ "You are an expert study coach creating clear, concise exam notes and practice questions.");
+ val userMessage =
+ Message.user(
+ "Generate a study guide for the {{?exam_type}} exam on {{?topic}}.\n\nInclude {{?num_questions}} practice questions.");
+ val templatingConfig = TemplateConfig.create().withMessages(systemMessage, userMessage);
+
+ val prompt = new OrchestrationPrompt(inputParams);
// list of supported language pairs
// https://help.sap.com/docs/translation-hub/sap-translation-hub/supported-languages?version=Cloud#translation-provider-sap-machine-translation
+
val configWithTranslation =
config
- .withInputTranslationConfig(TranslationConfig.translateInputTo("en-US"))
+ .withTemplateConfig(templatingConfig)
+ .withInputTranslationConfig(
+ TranslationConfig.translateInputTo("en-US")
+ .applyToPlaceholders("exam_type", "topic")
+ .withSourceLanguage("de-DE"))
.withOutputTranslationConfig(
- TranslationConfig.translateOutputTo("de-DE")
- .withSourceLanguage("en-US")); // optional source language
+ TranslationConfig.translateOutputTo("de-DE").withSourceLanguage("en-US"));
return client.chatCompletion(prompt, configWithTranslation);
}
diff --git a/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/OrchestrationTest.java b/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/OrchestrationTest.java
index 9f12ba226..0b2ada3c4 100644
--- a/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/OrchestrationTest.java
+++ b/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/OrchestrationTest.java
@@ -22,7 +22,6 @@
import com.sap.ai.sdk.orchestration.TemplateConfig;
import com.sap.ai.sdk.orchestration.TextItem;
import com.sap.ai.sdk.orchestration.model.DPIEntities;
-import com.sap.ai.sdk.orchestration.model.GenericModuleResult;
import com.sap.ai.sdk.orchestration.model.InputTranslationModuleResult;
import java.io.IOException;
import java.io.InputStream;
@@ -497,18 +496,22 @@ void testStreamingErrorHandlingMasking() {
void testTranslation() {
val result = service.translation();
val content = result.getContent();
- // English translated to German
- assertThat(content).contains("Englisch");
- assertThat(content).contains("Der", "ist");
+ // Output translation turns the model response back to German
+ assertThat(content)
+ .containsAnyOf("Abitur", "Deutsche", "Literatur", "Lern", "Übungs", "Fragen");
InputTranslationModuleResult inputTranslation =
result.getOriginalResponse().getIntermediateResults().getInputTranslation();
- GenericModuleResult outputTranslation =
- result.getOriginalResponse().getIntermediateResults().getOutputTranslation();
assertThat(inputTranslation).isNotNull();
- assertThat(outputTranslation).isNotNull();
assertThat(inputTranslation.getMessage())
- .isEqualTo("Translated messages with roles: ['user']. ");
+ .isNotNull()
+ .contains("Successfully translated placeholders:")
+ .contains("exam_type")
+ .contains("topic");
+
+ val outputTranslation =
+ result.getOriginalResponse().getIntermediateResults().getOutputTranslation();
+ assertThat(outputTranslation).isNotNull();
assertThat(outputTranslation.getMessage()).isEqualTo("Output Translation successful");
}