diff --git a/docs/release_notes.md b/docs/release_notes.md index 80e2325c3..b9af822c7 100644 --- a/docs/release_notes.md +++ b/docs/release_notes.md @@ -16,7 +16,7 @@ ### 📈 Improvements -- +- [Orchestration] Added new API `TranslationConfig#withApplyTo` to support partial translation for user's input. ### 🐛 Fixed Issues diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ApplyTo.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ApplyTo.java new file mode 100644 index 000000000..15554ce35 --- /dev/null +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ApplyTo.java @@ -0,0 +1,71 @@ +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 java.util.Objects; +import java.util.stream.Stream; +import javax.annotation.Nonnull; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * Convenience builder for {@link SAPDocumentTranslationApplyToSelector}. + * + *

This avoids passing raw strings for template roles and keeps sample-code readable. + */ +public final class ApplyTo { + /** + * Supported values for {@code items[]} when {@code category=template_roles}. + * + *

These map to the roles used in prompt templates. + */ + @RequiredArgsConstructor + public 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; + } + + /** + * Start an {@code apply_to} selector for placeholder names in {@code placeholder_values}. + * + * @param names The placeholder keys to translate. + * @return A selector with {@code category=placeholders} and the given items. + */ + @Nonnull + public static SAPDocumentTranslationApplyToSelector placeholders(@Nonnull final String... names) { + return SAPDocumentTranslationApplyToSelector.create().category(PLACEHOLDERS).items(names); + } + + /** + * Start an {@code apply_to} selector for prompt template message roles. + * + * @param roles The template roles to translate. + * @return A selector with {@code category=template_roles} and the given items. + */ + @Nonnull + public static SAPDocumentTranslationApplyToSelector templateRoles( + @Nonnull final TemplateRole... roles) { + final var roleStrings = + Stream.of(roles).filter(Objects::nonNull).map(TemplateRole::getValue).toList(); + + return SAPDocumentTranslationApplyToSelector.create() + .category(TEMPLATE_ROLES) + .items(roleStrings); + } +} 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..f830bd895 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,10 +1,12 @@ package com.sap.ai.sdk.orchestration; +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.List; import javax.annotation.Nonnull; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; @@ -29,13 +31,21 @@ class Input implements TranslationConfig { @With String sourceLanguage; - Object ApplyTo; // Can be null + /** + * Optional selection(s) to translate. If empty or null, translation is applied to the whole + * user input. + */ + @With 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); + + if (applyTo != null && !applyTo.isEmpty()) { + conf.applyTo(applyTo); + } + return SAPDocumentTranslationInput.create().type(translationType).config(conf); } } @@ -71,7 +81,6 @@ SAPDocumentTranslationOutput createSAPDocumentTranslationOutput() { */ @Nonnull static TranslationConfig.Input translateInputTo(@Nonnull final String targetLanguage) { - return new TranslationConfig.Input(targetLanguage, null, null); } @@ -86,7 +95,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..639e288e5 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 @@ -1,5 +1,10 @@ package com.sap.ai.sdk.orchestration; +import static com.sap.ai.sdk.orchestration.ApplyTo.TemplateRole.ASSISTANT; +import static com.sap.ai.sdk.orchestration.ApplyTo.TemplateRole.DEVELOPER; +import static com.sap.ai.sdk.orchestration.ApplyTo.TemplateRole.SYSTEM; +import static com.sap.ai.sdk.orchestration.ApplyTo.TemplateRole.TOOL; +import static com.sap.ai.sdk.orchestration.ApplyTo.TemplateRole.USER; 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; @@ -176,6 +181,39 @@ void testTranslationConfig() { .getSourceLanguage()); } + @Test + void testTranslationConfigApplyToSelectors() { + var selector = ApplyTo.placeholders("exam_type", "topic").sourceLanguage("de-DE"); + + final var inputTranslationConfig = + TranslationConfig.translateInputTo("en-US").withApplyTo(List.of(selector)); + + final var sapInput = inputTranslationConfig.createSAPDocumentTranslationInput(); + assertThat(sapInput.getConfig().getTargetLanguage()).isEqualTo("en-US"); + assertThat(sapInput.getConfig().getApplyTo()).hasSize(1); + assertThat(sapInput.getConfig().getApplyTo().get(0).getCategory().getValue()) + .isEqualTo("placeholders"); + assertThat(sapInput.getConfig().getApplyTo().get(0).getItems()) + .containsExactly("exam_type", "topic"); + + final var inputNull = TranslationConfig.translateInputTo("en-US"); + final var sapNull = inputNull.createSAPDocumentTranslationInput(); + assertThat(sapNull.getConfig().getApplyTo()).isEmpty(); + + // applyTo == empty list + final var inputEmpty = TranslationConfig.translateInputTo("en-US").withApplyTo(List.of()); + final var sapEmpty = inputEmpty.createSAPDocumentTranslationInput(); + assertThat(sapEmpty.getConfig().getApplyTo()).isEmpty(); + + selector = + ApplyTo.templateRoles(USER, SYSTEM, ASSISTANT, DEVELOPER, TOOL).sourceLanguage("de-DE"); + + assertThat(selector.getCategory().getValue()).isEqualTo("template_roles"); + assertThat(selector.getItems()) + .containsExactly("user", "system", "assistant", "developer", "tool"); + assertThat(selector.getSourceLanguage()).isEqualTo("de-DE"); + } + @Test void testParams() { // test withParams(Map) 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 45ed6797b..fdbe3c326 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 @@ -9,6 +9,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.sap.ai.sdk.core.AiCoreService; +import com.sap.ai.sdk.orchestration.ApplyTo; import com.sap.ai.sdk.orchestration.AzureContentFilter; import com.sap.ai.sdk.orchestration.AzureFilterThreshold; import com.sap.ai.sdk.orchestration.DpiMasking; @@ -688,17 +689,33 @@ 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") + .withApplyTo( + List.of( + // Translate only selected placeholder values from German to English + ApplyTo.placeholders("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"); }