diff --git a/orchestration/pom.xml b/orchestration/pom.xml index abcc51494..fd62fa900 100644 --- a/orchestration/pom.xml +++ b/orchestration/pom.xml @@ -39,8 +39,8 @@ 82% 94% 93% - 75% - 94% + 77% + 95% 100% diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ConfigToRequestTransformer.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ConfigToRequestTransformer.java index 4806489ce..f15dbacfa 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ConfigToRequestTransformer.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/ConfigToRequestTransformer.java @@ -14,7 +14,6 @@ import com.sap.ai.sdk.orchestration.model.PromptTemplatingModuleConfig; import com.sap.ai.sdk.orchestration.model.PromptTemplatingModuleConfigPrompt; import com.sap.ai.sdk.orchestration.model.Template; -import com.sap.ai.sdk.orchestration.model.TemplateRef; import com.sap.ai.sdk.orchestration.model.TranslationModuleConfig; import io.vavr.control.Option; import java.util.ArrayList; @@ -81,9 +80,6 @@ static PromptTemplatingModuleConfigPrompt toTemplateModuleConfig( * In this case, the request will fail, since the templating module will try to resolve the parameter. * To be fixed with https://github.tools.sap/AI/llm-orchestration/issues/662 */ - if (config instanceof TemplateRef) { - return config; - } val template = config instanceof Template t ? t : Template.create().template(); val messages = template.getTemplate(); @@ -249,4 +245,25 @@ static CompletionPostRequest fromReferenceToCompletionPostRequest( return request; } } + + @Nonnull + static CompletionRequestConfiguration fromTemplateRefToCompletionPostRequest( + @Nonnull final OrchestrationModuleConfig configWithRef) { + final OrchestrationTemplateReference templateRef = configWithRef.getTemplateRef(); + final var messageHistory = + templateRef.getMessagesHistory().stream().map(Message::createChatMessage).toList(); + final var placeholders = templateRef.getTemplateParameters(); + + final OrchestrationModuleConfig inner = + configWithRef.withTemplateConfig(templateRef.toLowLevel()); + + val requestConfig = + OrchestrationConfig.create().modules(toModuleConfigs(inner)).stream( + configWithRef.getGlobalStreamOptions()); + + return CompletionRequestConfiguration.create() + .config(requestConfig) + .placeholderValues(placeholders) + .messagesHistory(messageHistory); + } } diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClient.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClient.java index 955a04def..69eb20fa4 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClient.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClient.java @@ -192,6 +192,24 @@ public OrchestrationChatResponse chatCompletionUsingReference( return new OrchestrationChatResponse(response); } + /** + * Generate a completion using a module configuration containing a template reference Per-request + * history and parameters must be set on the template reference via {@link + * OrchestrationTemplateReference#withMessageHistory} and {@link + * OrchestrationTemplateReference#withTemplateParameters}. + * + * @param config A module configuration wrapping an {@link OrchestrationTemplateReference}. + * @return The completion output. + * @since 1.26.0 + */ + @Nonnull + public OrchestrationChatResponse chatCompletionUsingTemplateRef( + @Nonnull final OrchestrationModuleConfig config) { + val request = ConfigToRequestTransformer.fromTemplateRefToCompletionPostRequest(config); + val response = executeRequest(request); + return new OrchestrationChatResponse(response); + } + /** * Perform a request to the orchestration service using a module configuration provided as JSON * string. This can be useful when building a configuration in the AI Launchpad UI and exporting diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java index 82f4c6754..a29b773fa 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationModuleConfig.java @@ -136,6 +136,8 @@ public class OrchestrationModuleConfig { @Nullable SAPDocumentTranslationOutput outputTranslationConfig; + @Nullable OrchestrationTemplateReference templateRef; + /** Configuration of optional streaming options for output filtering. */ @With(AccessLevel.NONE) // may be exposed to public in the future @Getter(AccessLevel.PACKAGE) @@ -304,6 +306,7 @@ OrchestrationModuleConfig withOutputFilteringStreamOptions( this.groundingConfig, this.inputTranslationConfig, this.outputTranslationConfig, + this.templateRef, outputFilteringStreamOptions, this.globalStreamOptions); } @@ -340,6 +343,31 @@ public OrchestrationModuleConfig withTemplateConfig( return this.withTemplateConfig(templateConfig.toLowLevel()); } + /** + * Creates a new configuration with the given template reference. The template reference is the + * only source of prompt input + * + * @param templateRef The template reference to use. + * @return A new {@link OrchestrationModuleConfig} wrapping this config. + * @since 1.26.0 + */ + @Tolerate + @Nonnull + public OrchestrationModuleConfig withTemplateConfig( + @Nonnull final OrchestrationTemplateReference templateRef) { + return new OrchestrationModuleConfig( + this.llmConfig, + this.templateConfig, + this.maskingConfig, + this.filteringConfig, + this.groundingConfig, + this.inputTranslationConfig, + this.outputTranslationConfig, + templateRef, + outputFilteringStreamOptions, + this.globalStreamOptions); + } + /** * Configure input translation using a high-level TranslationConfig. * diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplateReference.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplateReference.java index 62fd0e067..922bb9677 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplateReference.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationTemplateReference.java @@ -5,10 +5,13 @@ import com.sap.ai.sdk.orchestration.model.TemplateRefByID; import com.sap.ai.sdk.orchestration.model.TemplateRefByScenarioNameVersion; import com.sap.ai.sdk.orchestration.model.TemplateRefTemplateRef; +import java.util.List; +import java.util.Map; import javax.annotation.Nonnull; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; +import lombok.Getter; import lombok.Value; import lombok.With; @@ -28,6 +31,46 @@ public class OrchestrationTemplateReference extends TemplateConfig { /** The scope of the template reference. */ @With @Nonnull ScopeEnum scope; + @Getter(AccessLevel.PACKAGE) + @Nonnull + List messagesHistory; + + @Getter(AccessLevel.PACKAGE) + @Nonnull + Map templateParameters; + + /** Build a template reference with scope only. */ + OrchestrationTemplateReference( + @Nonnull final TemplateRefTemplateRef reference, @Nonnull final ScopeEnum scope) { + this(reference, scope, List.of(), Map.of()); + } + + /** + * Set the chat history. + * + * @param messagesHistory The chat history to set. + * @return A new instance with the specified chat history. + */ + @Nonnull + public OrchestrationTemplateReference withMessageHistory( + @Nonnull final List messagesHistory) { + return new OrchestrationTemplateReference( + reference, scope, messagesHistory, templateParameters); + } + + /** + * Set the template parameters. + * + * @param templateParameters The template parameters to set. + * @return A new instance with the specified template parameters. + */ + @Nonnull + public OrchestrationTemplateReference withTemplateParameters( + @Nonnull final Map templateParameters) { + return new OrchestrationTemplateReference( + reference, scope, messagesHistory, templateParameters); + } + /** * Create a low-level representation of the template. * diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/ConfigToRequestTransformerTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/ConfigToRequestTransformerTest.java index e01415733..50ca2c29b 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/ConfigToRequestTransformerTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/ConfigToRequestTransformerTest.java @@ -205,4 +205,22 @@ void testUserMessageHistory() { assertThat(actual.getMessagesHistory()).containsExactly(userMessage.createChatMessage()); } + + @Test + void testToCompletionPostRequestWithRef() { + var ref = + TemplateConfig.reference() + .byId("test-id") + .withTemplateParameters(Map.of("lang", "DE")) + .withMessageHistory(List.of(new UserMessage("prev"))); + var config = + new OrchestrationModuleConfig() + .withLlmConfig(OrchestrationAiModel.GPT_4O) + .withTemplateConfig(ref); + + var request = ConfigToRequestTransformer.fromTemplateRefToCompletionPostRequest(config); + + assertThat(request.getPlaceholderValues()).containsEntry("lang", "DE"); + assertThat(request.getMessagesHistory()).hasSize(1); + } } 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 9cde42daa..800baa71e 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 @@ -375,4 +375,27 @@ void testResponseFormatOverwrittenByNewTemplateRef() { TemplateRef.create().templateRef(TemplateRefByID.create().id("123"))); assertThat(config.getTemplateConfig()).isInstanceOf(TemplateRef.class); } + + @Test + void withTemplateConfigReturnsWrapperWithRef() { + var ref = TemplateConfig.reference().byId("abc"); + OrchestrationModuleConfig withRef = + new OrchestrationModuleConfig() + .withLlmConfig(OrchestrationAiModel.GPT_4O) + .withTemplateConfig(ref); + + assertThat(withRef.getTemplateRef()).isSameAs(ref); + } + + @Test + void templateRefCarriesHistoryAndParams() { + var ref = + TemplateConfig.reference() + .byId("abc") + .withMessageHistory(List.of(new UserMessage("hi"))) + .withTemplateParameters(Map.of("k", "v")); + + assertThat(ref.getMessagesHistory()).hasSize(1); + assertThat(ref.getTemplateParameters()).containsEntry("k", "v"); + } } diff --git a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java index 713669abe..679041912 100644 --- a/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java +++ b/orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java @@ -1449,65 +1449,58 @@ void testResponseFormatText() throws IOException { @Test void testTemplateFromPromptRegistryByIdTenant() throws IOException { - { - stubFor( - post(anyUrl()) - .willReturn( - aResponse() - .withBodyFile("templateReferenceResponse.json") - .withHeader("Content-Type", "application/json"))); - - var template = TemplateConfig.reference().byId("21cb1358-0bf1-4f43-870b-00f14d0f9f16"); - var configWithTemplate = config.withTemplateConfig(template); - - var inputParams = Map.of("language", "Italian", "input", "Cloud ERP systems"); - var prompt = new OrchestrationPrompt(inputParams); - - final var response = client.chatCompletion(prompt, configWithTemplate); - assertThat(response.getContent()).startsWith("I sistemi ERP (Enterprise Resource Planning)"); - assertThat(response.getOriginalResponse().getIntermediateResults().getTemplating()) - .hasSize(2); - - final String request = fileLoaderStr.apply("templateReferenceByIdRequest.json"); - verify(postRequestedFor(anyUrl()).withRequestBody(equalToJson(request))); - } + stubFor( + post(anyUrl()) + .willReturn( + aResponse() + .withBodyFile("templateReferenceResponse.json") + .withHeader("Content-Type", "application/json"))); + + var inputParams = Map.of("language", "Italian", "input", "Cloud ERP systems"); + var template = + TemplateConfig.reference() + .byId("21cb1358-0bf1-4f43-870b-00f14d0f9f16") + .withTemplateParameters(inputParams); + var configWithTemplate = config.withTemplateConfig(template); + + final var response = client.chatCompletionUsingTemplateRef(configWithTemplate); + assertThat(response.getContent()).startsWith("I sistemi ERP (Enterprise Resource Planning)"); + assertThat(response.getOriginalResponse().getIntermediateResults().getTemplating()).hasSize(2); + + final String request = fileLoaderStr.apply("templateReferenceByIdRequest.json"); + verify(postRequestedFor(anyUrl()).withRequestBody(equalToJson(request))); } @Test void testTemplateFromPromptRegistryByIdResourceGroup() throws IOException { - { - stubFor( - post(anyUrl()) - .willReturn( - aResponse() - .withBodyFile("templateReferenceResourceGroupResponse.json") - .withHeader("Content-Type", "application/json"))); - - var template = - TemplateConfig.reference() - .byId("8bf72116-11ab-41bb-8933-8be56f59cb67") - .withScope(RESOURCE_GROUP); - var config = - new OrchestrationModuleConfig() - .withLlmConfig(GEMINI_2_5_FLASH.withParam(TEMPERATURE, 0.0)); - var configWithTemplate = config.withTemplateConfig(template); - - var inputParams = - Map.of( - "categories", - "Finance, Tech, Sports", - "inputExample", - "What's the latest news on the stock market?"); - var prompt = new OrchestrationPrompt(inputParams); - - final var response = client.chatCompletion(prompt, configWithTemplate); - assertThat(response.getContent()).startsWith("Finance"); - assertThat(response.getOriginalResponse().getIntermediateResults().getTemplating()) - .hasSize(2); - - final String request = fileLoaderStr.apply("templateReferenceResourceGroupByIdRequest.json"); - verify(postRequestedFor(anyUrl()).withRequestBody(equalToJson(request))); - } + stubFor( + post(anyUrl()) + .willReturn( + aResponse() + .withBodyFile("templateReferenceResourceGroupResponse.json") + .withHeader("Content-Type", "application/json"))); + + var inputParams = + Map.of( + "categories", + "Finance, Tech, Sports", + "inputExample", + "What's the latest news on the stock market?"); + var template = + TemplateConfig.reference() + .byId("8bf72116-11ab-41bb-8933-8be56f59cb67") + .withScope(RESOURCE_GROUP) + .withTemplateParameters(inputParams); + var config = + new OrchestrationModuleConfig().withLlmConfig(GEMINI_2_5_FLASH.withParam(TEMPERATURE, 0.0)); + var configWithTemplate = config.withTemplateConfig(template); + + final var response = client.chatCompletionUsingTemplateRef(configWithTemplate); + assertThat(response.getContent()).startsWith("Finance"); + assertThat(response.getOriginalResponse().getIntermediateResults().getTemplating()).hasSize(2); + + final String request = fileLoaderStr.apply("templateReferenceResourceGroupByIdRequest.json"); + verify(postRequestedFor(anyUrl()).withRequestBody(equalToJson(request))); } @Test @@ -1519,13 +1512,16 @@ void testTemplateFromPromptRegistryByScenarioTenant() throws IOException { .withBodyFile("templateReferenceResponse.json") .withHeader("Content-Type", "application/json"))); - var template = TemplateConfig.reference().byScenario("test").name("test").version("0.0.1"); - var configWithTemplate = config.withTemplateConfig(template); - var inputParams = Map.of("language", "Italian", "input", "Cloud ERP systems"); - var prompt = new OrchestrationPrompt(inputParams); + var template = + TemplateConfig.reference() + .byScenario("test") + .name("test") + .version("0.0.1") + .withTemplateParameters(inputParams); + var configWithTemplate = config.withTemplateConfig(template); - final var response = client.chatCompletion(prompt, configWithTemplate); + final var response = client.chatCompletionUsingTemplateRef(configWithTemplate); assertThat(response.getContent()).startsWith("I sistemi ERP (Enterprise Resource Planning)"); assertThat(response.getOriginalResponse().getIntermediateResults().getTemplating()).hasSize(2); @@ -1542,25 +1538,24 @@ void testTemplateFromPromptRegistryByScenarioResourceGroup() throws IOException .withBodyFile("templateReferenceResourceGroupResponse.json") .withHeader("Content-Type", "application/json"))); + var inputParams = + Map.of( + "categories", + "Finance, Tech, Sports", + "inputExample", + "What's the latest news on the stock market?"); var template = TemplateConfig.reference() .byScenario("categorization") .name("example-prompt-template") .version("0.0.1") - .withScope(RESOURCE_GROUP); + .withScope(RESOURCE_GROUP) + .withTemplateParameters(inputParams); var config = new OrchestrationModuleConfig().withLlmConfig(GEMINI_2_5_FLASH.withParam(TEMPERATURE, 0.0)); var configWithTemplate = config.withTemplateConfig(template); - var inputParams = - Map.of( - "categories", - "Finance, Tech, Sports", - "inputExample", - "What's the latest news on the stock market?"); - var prompt = new OrchestrationPrompt(inputParams); - - final var response = client.chatCompletion(prompt, configWithTemplate); + final var response = client.chatCompletionUsingTemplateRef(configWithTemplate); assertThat(response.getContent()).startsWith("Finance"); assertThat(response.getOriginalResponse().getIntermediateResults().getTemplating()).hasSize(2); @@ -1854,4 +1849,26 @@ void multiTurnReasoningPreservesReasoningContent() { postRequestedFor(urlPathEqualTo("/v2/completion")) .withRequestBody(equalToJson(expectedTurn2Request, true, true))); } + + @Test + void testChatCompletionWithRefConfigOnly() throws IOException { + stubFor( + post(anyUrl()) + .willReturn( + aResponse() + .withBodyFile("templateReferenceResponse.json") + .withHeader("Content-Type", "application/json"))); + + var ref = + TemplateConfig.reference() + .byId("21cb1358-0bf1-4f43-870b-00f14d0f9f16") + .withTemplateParameters(Map.of("language", "Italian", "input", "Cloud ERP systems")); + var configWithRef = config.withTemplateConfig(ref); + + final var response = client.chatCompletionUsingTemplateRef(configWithRef); + assertThat(response.getContent()).startsWith("I sistemi ERP (Enterprise Resource Planning)"); + + final String expectedRequest = fileLoaderStr.apply("templateReferenceByIdRequest.json"); + verify(postRequestedFor(anyUrl()).withRequestBody(equalToJson(expectedRequest))); + } } 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 088cfab5f..81b487bec 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 @@ -695,13 +695,14 @@ public OrchestrationChatResponse templateFromPromptRegistryByIdTenant( @Nonnull final String topic) { final var llmWithImageSupportConfig = new OrchestrationModuleConfig().withLlmConfig(GPT_5_MINI); - val template = TemplateConfig.reference().byId("21cb1358-0bf1-4f43-870b-00f14d0f9f16"); - val configWithTemplate = llmWithImageSupportConfig.withTemplateConfig(template); - val inputParams = Map.of("language", "Italian", "input", topic); - val prompt = new OrchestrationPrompt(inputParams); + val template = + TemplateConfig.reference() + .byId("21cb1358-0bf1-4f43-870b-00f14d0f9f16") + .withTemplateParameters(inputParams); + val configWithTemplate = llmWithImageSupportConfig.withTemplateConfig(template); - return client.chatCompletion(prompt, configWithTemplate); + return client.chatCompletionUsingTemplateRef(configWithTemplate); } /** @@ -720,16 +721,15 @@ public OrchestrationChatResponse templateFromPromptRegistryByIdResourceGroup( final var clientWithResourceGroup = client.withResourceGroup("ai-sdk-java-e2e", "orchestration"); + val inputParams = Map.of("categories", "Finance, Tech, Sports", "inputExample", inputExample); val template = TemplateConfig.reference() .byId("8bf72116-11ab-41bb-8933-8be56f59cb67") - .withScope(RESOURCE_GROUP); + .withScope(RESOURCE_GROUP) + .withTemplateParameters(inputParams); val configWithTemplate = config.withTemplateConfig(template); - val inputParams = Map.of("categories", "Finance, Tech, Sports", "inputExample", inputExample); - val prompt = new OrchestrationPrompt(inputParams); - - return clientWithResourceGroup.chatCompletion(prompt, configWithTemplate); + return clientWithResourceGroup.chatCompletionUsingTemplateRef(configWithTemplate); } /** @@ -744,13 +744,16 @@ public OrchestrationChatResponse templateFromPromptRegistryByIdResourceGroup( @Nonnull public OrchestrationChatResponse templateFromPromptRegistryByScenarioTenant( @Nonnull final String topic) { - val template = TemplateConfig.reference().byScenario("test").name("test").version("0.0.1"); - val configWithTemplate = config.withTemplateConfig(template); - val inputParams = Map.of("language", "Italian", "input", topic); - val prompt = new OrchestrationPrompt(inputParams); + val template = + TemplateConfig.reference() + .byScenario("test") + .name("test") + .version("0.0.1") + .withTemplateParameters(inputParams); + val configWithTemplate = config.withTemplateConfig(template); - return client.chatCompletion(prompt, configWithTemplate); + return client.chatCompletionUsingTemplateRef(configWithTemplate); } /** @@ -769,18 +772,17 @@ public OrchestrationChatResponse templateFromPromptRegistryByScenarioResourceGro final var clientWithResourceGroup = client.withResourceGroup("ai-sdk-java-e2e", "orchestration"); + val inputParams = Map.of("categories", "Finance, Tech, Sports", "inputExample", inputExample); val template = TemplateConfig.reference() .byScenario("categorization") .name("example-prompt-template") .version("0.0.1") - .withScope(RESOURCE_GROUP); + .withScope(RESOURCE_GROUP) + .withTemplateParameters(inputParams); val configWithTemplate = config.withTemplateConfig(template); - val inputParams = Map.of("categories", "Finance, Tech, Sports", "inputExample", inputExample); - val prompt = new OrchestrationPrompt(inputParams); - - return clientWithResourceGroup.chatCompletion(prompt, configWithTemplate); + return clientWithResourceGroup.chatCompletionUsingTemplateRef(configWithTemplate); } /**