Skip to content

Commit f91f075

Browse files
feat: [Orchestration] Translation
1 parent f21c732 commit f91f075

File tree

6 files changed

+136
-52
lines changed

6 files changed

+136
-52
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ static ModuleConfigs toModuleConfigs(@Nonnull final OrchestrationModuleConfig co
9595
Option.of(config.getFilteringConfig()).forEach(moduleConfig::filteringModuleConfig);
9696
Option.of(config.getMaskingConfig()).forEach(moduleConfig::maskingModuleConfig);
9797
Option.of(config.getGroundingConfig()).forEach(moduleConfig::groundingModuleConfig);
98+
Option.of(config.getOutputTranslationConfig())
99+
.forEach(moduleConfig::outputTranslationModuleConfig);
100+
Option.of(config.getInputTranslationConfig())
101+
.forEach(moduleConfig::inputTranslationModuleConfig);
98102

99103
return moduleConfig;
100104
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
import com.sap.ai.sdk.orchestration.model.FilteringModuleConfig;
55
import com.sap.ai.sdk.orchestration.model.GroundingModuleConfig;
66
import com.sap.ai.sdk.orchestration.model.InputFilteringConfig;
7+
import com.sap.ai.sdk.orchestration.model.InputTranslationModuleConfig;
78
import com.sap.ai.sdk.orchestration.model.LLMModuleConfig;
89
import com.sap.ai.sdk.orchestration.model.MaskingModuleConfig;
910
import com.sap.ai.sdk.orchestration.model.OutputFilteringConfig;
11+
import com.sap.ai.sdk.orchestration.model.OutputTranslationModuleConfig;
1012
import com.sap.ai.sdk.orchestration.model.TemplatingModuleConfig;
1113
import java.util.ArrayList;
1214
import java.util.Arrays;
@@ -94,6 +96,10 @@ public class OrchestrationModuleConfig {
9496
*/
9597
@Nullable GroundingModuleConfig groundingConfig;
9698

99+
@Nullable InputTranslationModuleConfig inputTranslationConfig;
100+
101+
@Nullable OutputTranslationModuleConfig outputTranslationConfig;
102+
97103
/**
98104
* Creates a new configuration with the given LLM configuration.
99105
*

sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,4 +291,14 @@ Object localPromptTemplate(@RequestParam(value = "format", required = false) fin
291291
}
292292
return response.getContent();
293293
}
294+
295+
@GetMapping("/translation")
296+
@Nonnull
297+
Object translation(@RequestParam(value = "format", required = false) final String format) {
298+
final var response = service.translation();
299+
if ("json".equals(format)) {
300+
return response;
301+
}
302+
return response.getContent();
303+
}
294304
}

sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.GEMINI_1_5_FLASH;
44
import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.GPT_4O_MINI;
55
import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.Parameter.TEMPERATURE;
6+
import static com.sap.ai.sdk.orchestration.model.SAPDocumentTranslation.TypeEnum.SAP_DOCUMENT_TRANSLATION;
67

78
import com.fasterxml.jackson.annotation.JsonProperty;
89
import com.sap.ai.sdk.core.AiCoreService;
@@ -26,6 +27,8 @@
2627
import com.sap.ai.sdk.orchestration.model.GroundingFilterSearchConfiguration;
2728
import com.sap.ai.sdk.orchestration.model.LlamaGuard38b;
2829
import com.sap.ai.sdk.orchestration.model.ResponseFormatText;
30+
import com.sap.ai.sdk.orchestration.model.SAPDocumentTranslation;
31+
import com.sap.ai.sdk.orchestration.model.SAPDocumentTranslationConfig;
2932
import com.sap.ai.sdk.orchestration.model.SearchDocumentKeyValueListPair;
3033
import com.sap.ai.sdk.orchestration.model.SearchSelectOptionEnum;
3134
import com.sap.ai.sdk.orchestration.model.Template;
@@ -531,4 +534,22 @@ public OrchestrationChatResponse localPromptTemplate(@Nonnull final String promp
531534

532535
return client.chatCompletion(prompt, configWithTemplate);
533536
}
537+
538+
public OrchestrationChatResponse translation() {
539+
val prompt = new OrchestrationPrompt("Quelle est la couleur de la tour Eiffel? Et en quelle langue tu me parles maintenant?");
540+
// list of supported language pairs
541+
// https://help.sap.com/docs/translation-hub/sap-translation-hub/supported-languages?version=Cloud#translation-provider-sap-machine-translation
542+
val configWithTranslation =
543+
config
544+
.withInputTranslationConfig(
545+
SAPDocumentTranslation.create()
546+
.type(SAP_DOCUMENT_TRANSLATION)
547+
.config(SAPDocumentTranslationConfig.create().targetLanguage("en-US")))
548+
.withOutputTranslationConfig(
549+
SAPDocumentTranslation.create()
550+
.type(SAP_DOCUMENT_TRANSLATION)
551+
.config(SAPDocumentTranslationConfig.create().targetLanguage("de-DE")));
552+
553+
return client.chatCompletion(prompt, configWithTranslation);
554+
}
534555
}

sample-code/spring-app/src/main/resources/static/index.html

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -509,18 +509,40 @@ <h2>Orchestration</h2>
509509
</div>
510510
</div>
511511
</li>
512-
<li class="list-group-item">
513-
<div class="info-tooltip">
514-
<button type="submit" formaction="/orchestration/localPromptTemplate"
515-
class="link-offset-2-hover link-underline link-underline-opacity-0 link-underline-opacity-75-hover endpoint">
516-
<code>/orchestration/localPromptTemplate</code>
517-
</button>
518-
<div class="tooltip-content">
519-
Chat request to an LLM through the Orchestration service using a local template file.
512+
<li class="list-group-item">
513+
<div class="info-tooltip">
514+
<button type="submit"
515+
formaction="/orchestration/localPromptTemplate"
516+
class="link-offset-2-hover link-underline link-underline-opacity-0 link-underline-opacity-75-hover endpoint">
517+
<code>/orchestration/localPromptTemplate</code>
518+
</button>
519+
<div class="tooltip-content">
520+
Chat request to an LLM through the Orchestration service
521+
using a local template file.
522+
</div>
520523
</div>
521-
</div>
522-
</li>
523-
</ul>
524+
</li>
525+
</ul>
526+
</details>
527+
<br>
528+
<details>
529+
<summary><h5 style="display:inline-block">Translation</h5>
530+
</summary>
531+
<ul class="list-group">
532+
<li class="list-group-item">
533+
<div class="info-tooltip">
534+
<button type="submit"
535+
formaction="/orchestration/translation"
536+
class="link-offset-2-hover link-underline link-underline-opacity-0 link-underline-opacity-75-hover endpoint">
537+
<code>/orchestration/translation</code>
538+
</button>
539+
<div class="tooltip-content">
540+
French question translated to English, sent to LLM, then
541+
translated to German.
542+
</div>
543+
</div>
544+
</li>
545+
</ul>
524546
</details>
525547
</div>
526548
</div>
@@ -710,7 +732,8 @@ <h5 class="mb-1">Orchestration Integration</h5>
710732
<code>/spring-ai-orchestration/inputFiltering/ALLOW_SAFE</code>
711733
</button>
712734
<div class="tooltip-content">
713-
Apply strict input filtering for a request to orchestration using the SpringAI integration.
735+
Apply strict input filtering for a request to orchestration
736+
using the SpringAI integration.
714737
</div>
715738
</div>
716739
</li>
@@ -722,7 +745,8 @@ <h5 class="mb-1">Orchestration Integration</h5>
722745
<code>/spring-ai-orchestration/outputFiltering/ALLOW_SAFE</code>
723746
</button>
724747
<div class="tooltip-content">
725-
Apply strict output filtering for a request to orchestration using the SpringAI integration.
748+
Apply strict output filtering for a request to orchestration
749+
using the SpringAI integration.
726750
</div>
727751
</div>
728752
</li>

sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/OrchestrationTest.java

Lines changed: 58 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.sap.ai.sdk.orchestration.TextItem;
2020
import com.sap.ai.sdk.orchestration.model.CompletionPostResponse;
2121
import com.sap.ai.sdk.orchestration.model.DPIEntities;
22+
import com.sap.ai.sdk.orchestration.model.GenericModuleResult;
2223
import com.sap.ai.sdk.orchestration.model.LLMChoice;
2324
import com.sap.ai.sdk.orchestration.model.LLMModuleResultSynchronous;
2425
import java.io.IOException;
@@ -32,16 +33,17 @@
3233
import java.util.Map;
3334
import java.util.concurrent.atomic.AtomicInteger;
3435
import lombok.extern.slf4j.Slf4j;
36+
import lombok.val;
3537
import org.junit.jupiter.api.BeforeEach;
3638
import org.junit.jupiter.api.Test;
3739
import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
3840

3941
@Slf4j
4042
class OrchestrationTest {
41-
OrchestrationService service;
4243
private final OrchestrationClient client = new OrchestrationClient();
4344
private final OrchestrationModuleConfig config =
4445
new OrchestrationModuleConfig().withLlmConfig(GEMINI_1_5_FLASH.withParam(TEMPERATURE, 0.0));
46+
OrchestrationService service;
4547

4648
@BeforeEach
4749
void setUp() {
@@ -50,18 +52,18 @@ void setUp() {
5052

5153
@Test
5254
void testCompletion() {
53-
final var result = service.completion("HelloWorld!");
55+
val result = service.completion("HelloWorld!");
5456

5557
assertThat(result).isNotNull();
5658
assertThat(result.getContent()).isNotEmpty();
5759
}
5860

5961
@Test
6062
void testStreamChatCompletion() {
61-
final var prompt = new OrchestrationPrompt("Who is the prettiest?");
62-
final var stream = new OrchestrationClient().streamChatCompletion(prompt, service.getConfig());
63+
val prompt = new OrchestrationPrompt("Who is the prettiest?");
64+
val stream = new OrchestrationClient().streamChatCompletion(prompt, service.getConfig());
6365

64-
final var filledDeltaCount = new AtomicInteger(0);
66+
val filledDeltaCount = new AtomicInteger(0);
6567
stream
6668
// foreach consumes all elements, closing the stream at the end
6769
.forEach(
@@ -80,10 +82,10 @@ void testStreamChatCompletion() {
8082
@Test
8183
void testTemplate() {
8284
assertThat(service.getConfig().getLlmConfig()).isNotNull();
83-
final var modelName = service.getConfig().getLlmConfig().getModelName();
85+
val modelName = service.getConfig().getLlmConfig().getModelName();
8486

85-
final var result = service.template("German");
86-
final var response = result.getOriginalResponse();
87+
val result = service.template("German");
88+
val response = result.getOriginalResponse();
8789

8890
assertThat(response.getRequestId()).isNotEmpty();
8991
assertThat(((TextItem) result.getAllMessages().get(0).content().items().get(0)).text())
@@ -124,7 +126,7 @@ void testTemplate() {
124126
void testMessagesHistory() {
125127
CompletionPostResponse result =
126128
service.messagesHistory("What is the capital of France?").getOriginalResponse();
127-
final var choices = ((LLMModuleResultSynchronous) result.getOrchestrationResult()).getChoices();
129+
val choices = ((LLMModuleResultSynchronous) result.getOrchestrationResult()).getChoices();
128130
assertThat(choices.get(0).getMessage().getContent()).isNotEmpty();
129131
}
130132

@@ -274,12 +276,12 @@ void testLlamaGuardDisabled() {
274276

275277
@Test
276278
void testImageInput() {
277-
final var result =
279+
val result =
278280
service
279281
.imageInput(
280282
"https://upload.wikimedia.org/wikipedia/commons/thumb/5/59/SAP_2011_logo.svg/440px-SAP_2011_logo.svg.png")
281283
.getOriginalResponse();
282-
final var choices = ((LLMModuleResultSynchronous) result.getOrchestrationResult()).getChoices();
284+
val choices = ((LLMModuleResultSynchronous) result.getOrchestrationResult()).getChoices();
283285
assertThat(choices.get(0).getMessage().getContent()).isNotEmpty();
284286
}
285287

@@ -297,80 +299,79 @@ void testImageInputBase64() {
297299
} catch (Exception e) {
298300
System.out.println("Error fetching or reading the image from URL: " + e.getMessage());
299301
}
300-
final var result = service.imageInput(dataUrl).getOriginalResponse();
301-
final var choices = ((LLMModuleResultSynchronous) result.getOrchestrationResult()).getChoices();
302+
val result = service.imageInput(dataUrl).getOriginalResponse();
303+
val choices = ((LLMModuleResultSynchronous) result.getOrchestrationResult()).getChoices();
302304
assertThat(choices.get(0).getMessage().getContent()).isNotEmpty();
303305
}
304306

305307
@Test
306308
void testMultiStringInput() {
307-
final var result =
309+
val result =
308310
service
309311
.multiStringInput(
310312
List.of("What is the capital of France?", "What is Chess about?", "What is 2+2?"))
311313
.getOriginalResponse();
312-
final var choices = ((LLMModuleResultSynchronous) result.getOrchestrationResult()).getChoices();
314+
val choices = ((LLMModuleResultSynchronous) result.getOrchestrationResult()).getChoices();
313315
assertThat(choices.get(0).getMessage().getContent()).isNotEmpty();
314316
}
315317

316318
@Test
317319
void testResponseFormatJsonSchema() {
318-
final var result = service.responseFormatJsonSchema("apple").getOriginalResponse();
319-
final var choices = ((LLMModuleResultSynchronous) result.getOrchestrationResult()).getChoices();
320+
val result = service.responseFormatJsonSchema("apple").getOriginalResponse();
321+
val choices = ((LLMModuleResultSynchronous) result.getOrchestrationResult()).getChoices();
320322
assertThat(choices.get(0).getMessage().getContent()).isNotEmpty();
321323
}
322324

323325
@Test
324326
void testResponseFormatJsonObject() {
325-
final var result = service.responseFormatJsonObject("apple").getOriginalResponse();
326-
final var choices = ((LLMModuleResultSynchronous) result.getOrchestrationResult()).getChoices();
327+
val result = service.responseFormatJsonObject("apple").getOriginalResponse();
328+
val choices = ((LLMModuleResultSynchronous) result.getOrchestrationResult()).getChoices();
327329
assertThat(choices.get(0).getMessage().getContent()).isNotEmpty();
328330
assertThat(choices.get(0).getMessage().getContent()).contains("\"language\":");
329331
assertThat(choices.get(0).getMessage().getContent()).contains("\"translation\":");
330332
}
331333

332334
@Test
333335
void testResponseFormatText() {
334-
final var result = service.responseFormatText("apple").getOriginalResponse();
335-
final var choices = ((LLMModuleResultSynchronous) result.getOrchestrationResult()).getChoices();
336+
val result = service.responseFormatText("apple").getOriginalResponse();
337+
val choices = ((LLMModuleResultSynchronous) result.getOrchestrationResult()).getChoices();
336338
assertThat(choices.get(0).getMessage().getContent()).isNotEmpty();
337339
}
338340

339341
@Test
340342
void testTemplateFromPromptRegistryById() {
341-
final var result =
342-
service.templateFromPromptRegistryById("Cloud ERP systems").getOriginalResponse();
343-
final var choices = ((LLMModuleResultSynchronous) result.getOrchestrationResult()).getChoices();
343+
val result = service.templateFromPromptRegistryById("Cloud ERP systems").getOriginalResponse();
344+
val choices = ((LLMModuleResultSynchronous) result.getOrchestrationResult()).getChoices();
344345
assertThat(choices.get(0).getMessage().getContent()).isNotEmpty();
345346
}
346347

347348
@Test
348349
void testTemplateFromPromptRegistryByScenario() {
349-
final var result =
350+
val result =
350351
service.templateFromPromptRegistryByScenario("Cloud ERP systems").getOriginalResponse();
351-
final var choices = ((LLMModuleResultSynchronous) result.getOrchestrationResult()).getChoices();
352+
val choices = ((LLMModuleResultSynchronous) result.getOrchestrationResult()).getChoices();
352353
assertThat(choices.get(0).getMessage().getContent()).isNotEmpty();
353354
}
354355

355356
@Test
356357
void testLocalPromptTemplate() throws IOException {
357-
final var result =
358+
val result =
358359
service
359360
.localPromptTemplate(
360361
Files.readString(Path.of("src/main/resources/promptTemplateExample.yaml")))
361362
.getOriginalResponse();
362-
final var choices = ((LLMModuleResultSynchronous) result.getOrchestrationResult()).getChoices();
363+
val choices = ((LLMModuleResultSynchronous) result.getOrchestrationResult()).getChoices();
363364
assertThat(choices.get(0).getMessage().getContent()).isNotEmpty();
364365
}
365366

366367
@Test
367368
void testStreamingErrorHandlingTemplate() {
368-
final var template = Message.user("Bad template: {{?language!@#$}}");
369-
final var templatingConfig =
369+
val template = Message.user("Bad template: {{?language!@#$}}");
370+
val templatingConfig =
370371
TemplateConfig.create().withTemplate(List.of(template.createChatMessage()));
371-
final var configWithTemplate = config.withTemplateConfig(templatingConfig);
372-
final var inputParams = Map.of("language", "German");
373-
final var prompt = new OrchestrationPrompt(inputParams);
372+
val configWithTemplate = config.withTemplateConfig(templatingConfig);
373+
val inputParams = Map.of("language", "German");
374+
val prompt = new OrchestrationPrompt(inputParams);
374375

375376
assertThatThrownBy(() -> client.streamChatCompletion(prompt, configWithTemplate))
376377
.isInstanceOf(OrchestrationClientException.class)
@@ -380,9 +381,9 @@ void testStreamingErrorHandlingTemplate() {
380381

381382
@Test
382383
void testStreamingErrorHandlingInputFilter() {
383-
final var prompt = new OrchestrationPrompt("Create 5 paraphrases of 'I hate you'.");
384-
final var filterConfig = new AzureContentFilter().hate(AzureFilterThreshold.ALLOW_SAFE);
385-
final var configWithFilter = config.withInputFiltering(filterConfig);
384+
val prompt = new OrchestrationPrompt("Create 5 paraphrases of 'I hate you'.");
385+
val filterConfig = new AzureContentFilter().hate(AzureFilterThreshold.ALLOW_SAFE);
386+
val configWithFilter = config.withInputFiltering(filterConfig);
386387

387388
assertThatThrownBy(() -> client.streamChatCompletion(prompt, configWithFilter))
388389
.isInstanceOf(OrchestrationClientException.class)
@@ -392,14 +393,32 @@ void testStreamingErrorHandlingInputFilter() {
392393

393394
@Test
394395
void testStreamingErrorHandlingMasking() {
395-
final var prompt = new OrchestrationPrompt("Some message.");
396-
final var maskingConfig =
396+
val prompt = new OrchestrationPrompt("Some message.");
397+
val maskingConfig =
397398
DpiMasking.anonymization().withEntities(DPIEntities.UNKNOWN_DEFAULT_OPEN_API);
398-
final var configWithMasking = config.withMaskingConfig(maskingConfig);
399+
val configWithMasking = config.withMaskingConfig(maskingConfig);
399400

400401
assertThatThrownBy(() -> client.streamChatCompletion(prompt, configWithMasking))
401402
.isInstanceOf(OrchestrationClientException.class)
402403
.hasMessageContaining("status 400 Bad Request")
403404
.hasMessageContaining("'type': 'sap_data_privacy_integration', 'method': 'anonymization'");
404405
}
406+
407+
@Test
408+
void testTranslation() {
409+
val result = service.translation();
410+
val content = result.getContent();
411+
// English translated to German
412+
assertThat(content).contains("Englisch");
413+
assertThat(content).contains("Der", "ist");
414+
415+
GenericModuleResult inputTranslation =
416+
result.getOriginalResponse().getModuleResults().getInputTranslation();
417+
GenericModuleResult outputTranslation =
418+
result.getOriginalResponse().getModuleResults().getOutputTranslation();
419+
assertThat(inputTranslation).isNotNull();
420+
assertThat(outputTranslation).isNotNull();
421+
assertThat(inputTranslation.getMessage()).isEqualTo("Input to LLM is translated successfully.");
422+
assertThat(outputTranslation.getMessage()).isEqualTo("Output Translation successful");
423+
}
405424
}

0 commit comments

Comments
 (0)