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"); }