diff --git a/core-services/prompt-registry/pom.xml b/core-services/prompt-registry/pom.xml index 5b9685867..1c17d1ff7 100644 --- a/core-services/prompt-registry/pom.xml +++ b/core-services/prompt-registry/pom.xml @@ -38,11 +38,11 @@ ${project.basedir}/../../ - 73% - 87% - 89% - 75% - 75% + 85% + 91% + 93% + 100% + 81% 100% @@ -97,6 +97,14 @@ com.google.code.findbugs jsr305 + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + com.fasterxml.jackson.core + jackson-core + org.projectlombok diff --git a/core-services/prompt-registry/src/main/java/com/sap/ai/sdk/prompt/registry/OrchestrationConfigClient.java b/core-services/prompt-registry/src/main/java/com/sap/ai/sdk/prompt/registry/OrchestrationConfigClient.java new file mode 100644 index 000000000..321c799fe --- /dev/null +++ b/core-services/prompt-registry/src/main/java/com/sap/ai/sdk/prompt/registry/OrchestrationConfigClient.java @@ -0,0 +1,108 @@ +package com.sap.ai.sdk.prompt.registry; + +import static com.sap.ai.sdk.core.JacksonConfiguration.getDefaultObjectMapper; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.google.common.annotations.Beta; +import com.google.common.collect.Iterables; +import com.sap.ai.sdk.core.AiCoreService; +import com.sap.ai.sdk.prompt.registry.client.OrchestrationConfigsApi; +import com.sap.ai.sdk.prompt.registry.model.AzureContentSafetyInputFilterConfig; +import com.sap.ai.sdk.prompt.registry.model.AzureContentSafetyOutputFilterConfig; +import com.sap.ai.sdk.prompt.registry.model.InputFilterConfig; +import com.sap.ai.sdk.prompt.registry.model.LlamaGuard38bFilterConfig; +import com.sap.ai.sdk.prompt.registry.model.OutputFilterConfig; +import com.sap.cloud.sdk.cloudplatform.connectivity.ApacheHttpClient5Accessor; +import com.sap.cloud.sdk.services.openapi.apiclient.ApiClient; +import javax.annotation.Nonnull; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.springframework.http.client.BufferingClientHttpRequestFactory; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.http.converter.yaml.MappingJackson2YamlHttpMessageConverter; +import org.springframework.web.client.RestTemplate; + +/** + * Client for managing Orchestration Configurations in the Prompt Registry service. + * + * @since 1.15.0 + */ +@Beta +public class OrchestrationConfigClient extends OrchestrationConfigsApi { + + /** + * Instantiates a client to manage Orchestration Configurations on the Prompt Registry service. + */ + public OrchestrationConfigClient() { + this(new AiCoreService()); + } + + /** + * Instantiates a client to manage Orchestration Configurations on the Prompt Registry service. + * + * @param aiCoreService The configured connectivity instance to AI Core + */ + public OrchestrationConfigClient(@Nonnull final AiCoreService aiCoreService) { + super(addMixin(aiCoreService)); + } + + @Nonnull + private static ApiClient addMixin(@Nonnull final AiCoreService service) { + final var destination = service.getBaseDestination(); + final var httpRequestFactory = new HttpComponentsClientHttpRequestFactory(); + httpRequestFactory.setHttpClient(ApacheHttpClient5Accessor.getHttpClient(destination)); + + final var rt = new RestTemplate(); + Iterables.filter(rt.getMessageConverters(), MappingJackson2HttpMessageConverter.class) + .forEach( + converter -> + converter.setObjectMapper( + getDefaultObjectMapper() + .addMixIn(OutputFilterConfig.class, JacksonMixin.OutputFilter.class) + .addMixIn(InputFilterConfig.class, JacksonMixin.InputFilter.class))); + final var yamlMapper = new ObjectMapper(new YAMLFactory()); + yamlMapper + .addMixIn(OutputFilterConfig.class, JacksonMixin.OutputFilter.class) + .addMixIn(InputFilterConfig.class, JacksonMixin.InputFilter.class); + Iterables.filter(rt.getMessageConverters(), MappingJackson2YamlHttpMessageConverter.class) + .forEach(converter -> converter.setObjectMapper(yamlMapper)); + + rt.setRequestFactory(new BufferingClientHttpRequestFactory(httpRequestFactory)); + + return new ApiClient(rt).setBasePath(destination.asHttp().getUri().toString()); + } + + @NoArgsConstructor(access = AccessLevel.PRIVATE) + private static class JacksonMixin { + + @JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + property = "type", + visible = true) + @JsonSubTypes({ + @JsonSubTypes.Type(value = LlamaGuard38bFilterConfig.class, name = "llama_guard_3_8b"), + @JsonSubTypes.Type( + value = AzureContentSafetyOutputFilterConfig.class, + name = "azure_content_safety") + }) + interface OutputFilter {} + + @JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + property = "type", + visible = true) + @JsonSubTypes({ + @JsonSubTypes.Type(value = LlamaGuard38bFilterConfig.class, name = "llama_guard_3_8b"), + @JsonSubTypes.Type( + value = AzureContentSafetyInputFilterConfig.class, + name = "azure_content_safety") + }) + interface InputFilter {} + } +} diff --git a/core-services/prompt-registry/src/test/java/com/sap/ai/sdk/prompt/registry/OrchestrationConfigClientTest.java b/core-services/prompt-registry/src/test/java/com/sap/ai/sdk/prompt/registry/OrchestrationConfigClientTest.java new file mode 100644 index 000000000..2365d0bdc --- /dev/null +++ b/core-services/prompt-registry/src/test/java/com/sap/ai/sdk/prompt/registry/OrchestrationConfigClientTest.java @@ -0,0 +1,43 @@ +package com.sap.ai.sdk.prompt.registry; + +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.assertj.core.api.Assertions.assertThat; + +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; +import com.sap.ai.sdk.core.AiCoreService; +import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; +import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class OrchestrationConfigClientTest { + @RegisterExtension + private static final WireMockExtension WM = + WireMockExtension.newInstance().options(wireMockConfig().dynamicPort()).build(); + + private static OrchestrationConfigClient client; + + @BeforeEach + void setup() { + final HttpDestination destination = DefaultHttpDestination.builder(WM.baseUrl()).build(); + final AiCoreService service = new AiCoreService().withBaseDestination(destination); + client = new OrchestrationConfigClient(service); + } + + @Test + void testPipelines() { + final var result = client.listOrchestrationConfigs(); + assertThat(result.getCount()).isEqualTo(2); + assertThat(result.getResources()).hasSize(2); + final var template = result.getResources().get(0); + assertThat(template.getId()).isEqualTo(UUID.fromString("62e8638a-ae87-4bd5-9027-a0bc67db1609")); + assertThat(template.getName()).isEqualTo("test-config-for-OrchestrationTest"); + assertThat(template.getVersion()).isEqualTo("0.0.1"); + assertThat(template.getScenario()).isEqualTo("sdk-test-scenario"); + assertThat(template.getCreationTimestamp()).isEqualTo("2025-12-19T16:24:27.442000"); + assertThat(template.getManagedBy()).isEqualTo("imperative"); + assertThat(template.isIsVersionHead()).isEqualTo(true); + } +} diff --git a/core-services/prompt-registry/src/test/resources/mappings/orchestrationConfigs.json b/core-services/prompt-registry/src/test/resources/mappings/orchestrationConfigs.json new file mode 100644 index 000000000..f376835b7 --- /dev/null +++ b/core-services/prompt-registry/src/test/resources/mappings/orchestrationConfigs.json @@ -0,0 +1,35 @@ +{ + "request": { + "method": "GET", + "url": "/v2/registry/v2/orchestrationConfigs" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "jsonBody": { + "count": 2, + "resources": [ + { + "id": "62e8638a-ae87-4bd5-9027-a0bc67db1609", + "name": "test-config-for-OrchestrationTest", + "version": "0.0.1", + "scenario": "sdk-test-scenario", + "creation_timestamp": "2025-12-19T16:24:27.442000", + "managed_by": "imperative", + "is_version_head": true + }, + { + "id": "f9f2875a-4c92-471b-a403-51a50e70fe52", + "name": "test-config", + "version": "0.0.1", + "scenario": "sdk-test-scenario", + "creation_timestamp": "2025-12-19T16:33:05.607000", + "managed_by": "imperative", + "is_version_head": true + } + ] + } + } +} diff --git a/docs/release_notes.md b/docs/release_notes.md index 5745d6681..b41b78f96 100644 --- a/docs/release_notes.md +++ b/docs/release_notes.md @@ -12,7 +12,8 @@ ### ✨ New Functionality -- +- [Orchestration] Configs stored in prompt registry can now be used for [Orchestration calls via reference](https://sap.github.io/ai-sdk/docs/java/guides/orchestration-chat-completion#using-a-prepared-configuration). +- [Prompt Registry] Added support to [manage Orchestration configs stored in Prompt Registry](https://sap.github.io/ai-sdk/docs/java/ai-core/prompt-registry#orchestration-configurations-in-prompt-registry). ### 📈 Improvements diff --git a/orchestration/pom.xml b/orchestration/pom.xml index 40a19d8dc..0b46e646e 100644 --- a/orchestration/pom.xml +++ b/orchestration/pom.xml @@ -36,11 +36,11 @@ ${project.basedir}/../ - 80% - 94% + 82% + 95% 93% - 74% - 93% + 75% + 94% 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 cbaeac33e..8cd40c309 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 @@ -1,6 +1,11 @@ package com.sap.ai.sdk.orchestration; +import com.sap.ai.sdk.orchestration.model.CompletionPostRequest; import com.sap.ai.sdk.orchestration.model.CompletionRequestConfiguration; +import com.sap.ai.sdk.orchestration.model.CompletionRequestConfigurationReferenceById; +import com.sap.ai.sdk.orchestration.model.CompletionRequestConfigurationReferenceByIdConfigRef; +import com.sap.ai.sdk.orchestration.model.CompletionRequestConfigurationReferenceByNameScenarioVersion; +import com.sap.ai.sdk.orchestration.model.CompletionRequestConfigurationReferenceByNameScenarioVersionConfigRef; import com.sap.ai.sdk.orchestration.model.ModuleConfigs; import com.sap.ai.sdk.orchestration.model.OrchestrationConfig; import com.sap.ai.sdk.orchestration.model.OrchestrationConfigModules; @@ -16,9 +21,11 @@ import javax.annotation.Nullable; import lombok.AccessLevel; import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; import lombok.val; /** Factory to create all data objects from an orchestration configuration. */ +@Slf4j @NoArgsConstructor(access = AccessLevel.NONE) final class ConfigToRequestTransformer { @Nonnull @@ -113,4 +120,37 @@ static InnerModuleConfigs toModuleConfigs(@Nonnull final OrchestrationModuleConf return OrchestrationConfigModules.createInnerModuleConfigs(moduleConfig); } + + @Nonnull + static CompletionPostRequest fromReferenceToCompletionPostRequest( + @Nonnull final OrchestrationConfigReference reference) { + final OrchestrationPrompt prompt = reference.getPrompt(); + final var messageHistory = + prompt.getMessagesHistory().stream().map(Message::createChatMessage).toList(); + final var placeholders = prompt.getTemplateParameters(); + + CompletionPostRequest request; + if (reference.getId() != null) { + request = + CompletionRequestConfigurationReferenceById.create() + .configRef( + CompletionRequestConfigurationReferenceByIdConfigRef.create() + .id(reference.getId())); + ((CompletionRequestConfigurationReferenceById) request).setMessagesHistory(messageHistory); + ((CompletionRequestConfigurationReferenceById) request).setPlaceholderValues(placeholders); + } else { + request = + CompletionRequestConfigurationReferenceByNameScenarioVersion.create() + .configRef( + CompletionRequestConfigurationReferenceByNameScenarioVersionConfigRef.create() + .scenario(reference.getScenario()) + .name(reference.getName()) + .version(reference.getVersion())); + ((CompletionRequestConfigurationReferenceByNameScenarioVersion) request) + .setMessagesHistory(messageHistory); + ((CompletionRequestConfigurationReferenceByNameScenarioVersion) request) + .setPlaceholderValues(placeholders); + } + return request; + } } 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 6ec9de4cf..66fe58bf1 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 @@ -166,6 +166,22 @@ public CompletionPostResponse executeRequest(@Nonnull final CompletionPostReques COMPLETION_ENDPOINT, request, CompletionPostResponse.class, customHeaders); } + /** + * Generate a completion using a referenced Orchestration config. + * + * @param reference A reference to an Orchestration config stored in prompt registry + * @return The completion output + * @since 1.15.0 + */ + @Beta + @Nonnull + public OrchestrationChatResponse chatCompletionUsingReference( + @Nonnull final OrchestrationConfigReference reference) { + val request = ConfigToRequestTransformer.fromReferenceToCompletionPostRequest(reference); + 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/OrchestrationConfigReference.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationConfigReference.java new file mode 100644 index 000000000..59a4bae9d --- /dev/null +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationConfigReference.java @@ -0,0 +1,110 @@ +package com.sap.ai.sdk.orchestration; + +import com.google.common.annotations.Beta; +import java.util.List; +import java.util.Map; +import javax.annotation.Nonnull; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Value; + +/** + * Class representing a reference to an Orchestration config stored in prompt registry. + * + * @since 1.15.0 + */ +@Value +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Getter(AccessLevel.PACKAGE) +@Beta +public class OrchestrationConfigReference { + String id; + String scenario; + String name; + String version; + OrchestrationPrompt prompt = new OrchestrationPrompt(Map.of()); + + /** + * Set the chat history for this reference. + * + * @param messagesHistory The chat history to set. + * @return The current instance of {@link OrchestrationConfigReference} with the changed chat + * history. + */ + @Nonnull + public OrchestrationConfigReference messageHistory(@Nonnull final List messagesHistory) { + prompt.messageHistory(messagesHistory); + return this; + } + + /** + * Set the template parameters for this reference. + * + * @param templateParameters The template parameters to set. + * @return The current instance of {@link OrchestrationConfigReference} with the changed chat + * history. + */ + @Nonnull + public OrchestrationConfigReference templateParameters( + @Nonnull final Map templateParameters) { + prompt.templateParameters(templateParameters); + return this; + } + + /** + * Build a reference from an ID. + * + * @param id The id of the reference + * @return A reference object with the specified id + */ + @Nonnull + public static OrchestrationConfigReference fromId(@Nonnull final String id) { + return new OrchestrationConfigReference(id, null, null, null); + } + + /** + * Build a reference from a scenario, name, and version. + * + * @param scenario The scenario of the reference + * @return A builder object with the specified scenario + */ + @Nonnull + public static Builder fromScenario(@Nonnull final String scenario) { + return (name) -> (version) -> new OrchestrationConfigReference(null, scenario, name, version); + } + + /** + * Builder to create an Orchestration config reference from scenario, name, and version. + * + * @since 1.14.0 + */ + public interface Builder { + + /** + * Build a reference from a scenario, name, and version. + * + * @param name The name of the reference + * @return A builder object with the specified scenario and name + */ + @Nonnull + Builder1 name(@Nonnull final String name); + } + + /** + * Builder to create an Orchestration config reference from scenario, name, and version. + * + * @since 1.14.0 + */ + public interface Builder1 { + + /** + * Build a reference from a scenario, name, and version. + * + * @param version The version of the reference + * @return A reference object with the specified scenario, name, and version + */ + @Nonnull + OrchestrationConfigReference version(@Nonnull final String version); + } +} diff --git a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationPrompt.java b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationPrompt.java index deb3ec40e..01dc4d670 100644 --- a/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationPrompt.java +++ b/orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationPrompt.java @@ -69,4 +69,18 @@ public OrchestrationPrompt messageHistory(@Nonnull final List messagesH this.messagesHistory.addAll(messagesHistory); return this; } + + /** + * Set the template parameters of this prompt. + * + * @param templateParameters The template parameters to add. + * @return The current instance of {@link OrchestrationPrompt} with the changed template + * parameters. + */ + @Nonnull + OrchestrationPrompt templateParameters(@Nonnull final Map templateParameters) { + this.templateParameters.clear(); + this.templateParameters.putAll(templateParameters); + return this; + } } 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 6bfd757af..d4a6afb09 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 @@ -1347,6 +1347,62 @@ void testTemplateFromInputThrows() { .hasMessageContaining("Failed to deserialize"); } + @Test + void testExecuteFromReferenceById() { + stubFor( + post(anyUrl()) + .willReturn( + aResponse() + .withBodyFile("templatingResponse.json") + .withHeader("Content-Type", "application/json"))); + + var reference = OrchestrationConfigReference.fromId("test-id"); + final var response = client.chatCompletionUsingReference(reference); + + final String expectedRequest = fileLoaderStr.apply("orchConfigByIdRequest.json"); + verify(postRequestedFor(anyUrl()).withRequestBody(equalToJson(expectedRequest))); + } + + @Test + void testExecuteFromReferenceBySNV() { + stubFor( + post(anyUrl()) + .willReturn( + aResponse() + .withBodyFile("templatingResponse.json") + .withHeader("Content-Type", "application/json"))); + + var reference = + OrchestrationConfigReference.fromScenario("scenario").name("name").version("0.0.1"); + final var response = client.chatCompletionUsingReference(reference); + + final String expectedRequest = fileLoaderStr.apply("orchConfigBySNVRequest.json"); + verify(postRequestedFor(anyUrl()).withRequestBody(equalToJson(expectedRequest))); + } + + @Test + void testExecuteFromReferenceWithMessageHistoryAndInputParams() { + stubFor( + post(anyUrl()) + .willReturn( + aResponse() + .withBodyFile("templatingResponse.json") + .withHeader("Content-Type", "application/json"))); + + List history = List.of(new SystemMessage("System Message")); + var params = Map.of("placeholder", "value"); + var reference = + OrchestrationConfigReference.fromScenario("scenario") + .name("name") + .version("0.0.1") + .messageHistory(history) + .templateParameters(params); + final var response = client.chatCompletionUsingReference(reference); + + final String expectedRequest = fileLoaderStr.apply("orchConfigByRequestHistoryParams.json"); + verify(postRequestedFor(anyUrl()).withRequestBody(equalToJson(expectedRequest))); + } + @Test void testGetAllMessages() { stubFor( diff --git a/orchestration/src/test/resources/orchConfigByIdRequest.json b/orchestration/src/test/resources/orchConfigByIdRequest.json new file mode 100644 index 000000000..9c919cb4b --- /dev/null +++ b/orchestration/src/test/resources/orchConfigByIdRequest.json @@ -0,0 +1,7 @@ +{ + "config_ref": { + "id": "test-id" + }, + "placeholder_values": {}, + "messages_history": [] +} \ No newline at end of file diff --git a/orchestration/src/test/resources/orchConfigByRequestHistoryParams.json b/orchestration/src/test/resources/orchConfigByRequestHistoryParams.json new file mode 100644 index 000000000..08e115741 --- /dev/null +++ b/orchestration/src/test/resources/orchConfigByRequestHistoryParams.json @@ -0,0 +1,14 @@ +{ + "config_ref" : { + "scenario" : "scenario", + "name" : "name", + "version" : "0.0.1" + }, + "placeholder_values" : { + "placeholder" : "value" + }, + "messages_history" : [ { + "role" : "system", + "content" : "System Message" + } ] +} \ No newline at end of file diff --git a/orchestration/src/test/resources/orchConfigBySNVRequest.json b/orchestration/src/test/resources/orchConfigBySNVRequest.json new file mode 100644 index 000000000..c8218d6d4 --- /dev/null +++ b/orchestration/src/test/resources/orchConfigBySNVRequest.json @@ -0,0 +1,9 @@ +{ + "config_ref": { + "scenario": "scenario", + "name": "name", + "version": "0.0.1" + }, + "placeholder_values": {}, + "messages_history": [] +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index a6b0ab581..0a8fa755e 100644 --- a/pom.xml +++ b/pom.xml @@ -561,6 +561,10 @@ org.junit.jupiter:junit-jupiter-engine + + + com.fasterxml.jackson.dataformat:jackson-dataformat-yaml + diff --git a/sample-code/spring-app/pom.xml b/sample-code/spring-app/pom.xml index 7a23b70d1..6b9bd2199 100644 --- a/sample-code/spring-app/pom.xml +++ b/sample-code/spring-app/pom.xml @@ -236,7 +236,6 @@ com.fasterxml.jackson.dataformat jackson-dataformat-yaml - test diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java index f9ad72596..b856a6264 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/OrchestrationController.java @@ -361,4 +361,14 @@ Object embedding(@RequestParam(value = "format", required = false) final String } return response.getEmbeddingVectors(); } + + @GetMapping("/configFromRegistry") + @Nonnull + Object configFromRegistry(@RequestParam(value = "format", required = false) final String format) { + final var response = service.executeConfigFromReference(); + if ("json".equals(format)) { + return response; + } + return response.getContent(); + } } diff --git a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/PromptRegistryController.java b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/PromptRegistryController.java index b2c79d0fc..4857924eb 100644 --- a/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/PromptRegistryController.java +++ b/sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/controllers/PromptRegistryController.java @@ -3,7 +3,16 @@ import com.sap.ai.sdk.foundationmodels.openai.OpenAiClient; import com.sap.ai.sdk.foundationmodels.openai.OpenAiModel; import com.sap.ai.sdk.foundationmodels.openai.spring.OpenAiChatModel; +import com.sap.ai.sdk.prompt.registry.OrchestrationConfigClient; import com.sap.ai.sdk.prompt.registry.PromptClient; +import com.sap.ai.sdk.prompt.registry.model.LLMModelDetails; +import com.sap.ai.sdk.prompt.registry.model.ModuleConfigs; +import com.sap.ai.sdk.prompt.registry.model.OrchestrationConfig; +import com.sap.ai.sdk.prompt.registry.model.OrchestrationConfigDeleteResponse; +import com.sap.ai.sdk.prompt.registry.model.OrchestrationConfigListResponse; +import com.sap.ai.sdk.prompt.registry.model.OrchestrationConfigModules; +import com.sap.ai.sdk.prompt.registry.model.OrchestrationConfigPostRequest; +import com.sap.ai.sdk.prompt.registry.model.OrchestrationConfigPostResponse; import com.sap.ai.sdk.prompt.registry.model.PromptTemplateDeleteResponse; import com.sap.ai.sdk.prompt.registry.model.PromptTemplateListResponse; import com.sap.ai.sdk.prompt.registry.model.PromptTemplatePostRequest; @@ -11,7 +20,11 @@ import com.sap.ai.sdk.prompt.registry.model.PromptTemplateSpec; import com.sap.ai.sdk.prompt.registry.model.PromptTemplateSubstitutionRequest; import com.sap.ai.sdk.prompt.registry.model.PromptTemplateSubstitutionResponse; +import com.sap.ai.sdk.prompt.registry.model.PromptTemplatingModuleConfig; import com.sap.ai.sdk.prompt.registry.model.SingleChatTemplate; +import com.sap.ai.sdk.prompt.registry.model.Template; +import com.sap.ai.sdk.prompt.registry.model.UserChatMessage; +import com.sap.ai.sdk.prompt.registry.model.UserChatMessageContent; import com.sap.ai.sdk.prompt.registry.spring.SpringAiConverter; import java.io.IOException; import java.util.List; @@ -36,23 +49,24 @@ @RequestMapping("/prompt-registry") class PromptRegistryController { static final String NAME = "java-e2e-test"; - private static final PromptClient client = new PromptClient(); + private static final PromptClient promptClient = new PromptClient(); + private static final OrchestrationConfigClient orchConfigClient = new OrchestrationConfigClient(); @GetMapping("/listTemplates") PromptTemplateListResponse listTemplates() { - return client.listPromptTemplates(); + return promptClient.listPromptTemplates(); } @GetMapping("/createTemplate") PromptTemplatePostResponse createTemplate() { - return client.createUpdatePromptTemplate(getTemplate("Finance, Tech, Sports")); + return promptClient.createUpdatePromptTemplate(getTemplate("Finance, Tech, Sports")); } @GetMapping("/updateTemplate") PromptTemplatePostResponse updateTemplate() { // create template then update - client.createUpdatePromptTemplate(getTemplate("Finance, Tech, Sports")); - return client.createUpdatePromptTemplate(getTemplate("Finance, Tech, Sports, Politics")); + promptClient.createUpdatePromptTemplate(getTemplate("Finance, Tech, Sports")); + return promptClient.createUpdatePromptTemplate(getTemplate("Finance, Tech, Sports, Politics")); } private PromptTemplatePostRequest getTemplate(final String categories) { @@ -75,25 +89,25 @@ private PromptTemplatePostRequest getTemplate(final String categories) { @GetMapping("/history") PromptTemplateListResponse history() { - return client.listPromptTemplateHistory("categorization", "0.0.1", NAME); + return promptClient.listPromptTemplateHistory("categorization", "0.0.1", NAME); } @GetMapping("/importTemplate") PromptTemplatePostResponse importTemplate() throws IOException { final Resource template = new ClassPathResource("prompt-template.yaml"); - return client.importPromptTemplate("default", null, template); + return promptClient.importPromptTemplate("default", null, template); } @GetMapping("/exportTemplate") byte[] exportTemplate() throws IOException { final var template = importTemplate(); - return client.exportPromptTemplate(template.getId()); + return promptClient.exportPromptTemplate(template.getId()); } @GetMapping("/useTemplate") PromptTemplateSubstitutionResponse useTemplate() { final var template = createTemplate(); - return client.parsePromptTemplateById( + return promptClient.parsePromptTemplateById( template.getId(), "default", null, @@ -104,11 +118,11 @@ PromptTemplateSubstitutionResponse useTemplate() { @GetMapping("/deleteTemplate") List deleteTemplate() { - final PromptTemplateListResponse templates = client.listPromptTemplates(); + final PromptTemplateListResponse templates = promptClient.listPromptTemplates(); return templates.getResources().stream() .filter(template -> NAME.equals(template.getName())) - .map(template -> client.deletePromptTemplate(template.getId())) + .map(template -> promptClient.deletePromptTemplate(template.getId())) .toList(); } @@ -137,4 +151,47 @@ Generation promptRegistryToSpringAi() { val response = cl.prompt(prompt).call().chatResponse(); return response != null ? response.getResult() : null; } + + @GetMapping("/listOrchConfigs") + OrchestrationConfigListResponse listOrchConfigs() { + return orchConfigClient.listOrchestrationConfigs(); + } + + @GetMapping("/createOrchConfig") + OrchestrationConfigPostResponse createOrchConfig() { + final OrchestrationConfigPostRequest postRequest = + OrchestrationConfigPostRequest.create() + .name(NAME) + .version("0.0.1") + .scenario("sdk-test-scenario") + .spec(buildOrchestrationConfig()); + return orchConfigClient.createUpdateOrchestrationConfig(postRequest); + } + + @GetMapping("/deleteOrchConfig") + List deleteOrchConfig() { + final OrchestrationConfigListResponse configs = orchConfigClient.listOrchestrationConfigs(); + + return configs.getResources().stream() + .filter(config -> NAME.equals(config.getName())) + .map(config -> orchConfigClient.deleteOrchestrationConfig(config.getId())) + .toList(); + } + + private OrchestrationConfig buildOrchestrationConfig() { + return OrchestrationConfig.create() + .modules( + OrchestrationConfigModules.createInnerModuleConfigs( + ModuleConfigs.create() + .promptTemplating( + PromptTemplatingModuleConfig.create() + .prompt( + Template.create() + .template( + UserChatMessage.create() + .content( + new UserChatMessageContent.InnerString("message")) + .role(UserChatMessage.RoleEnum.USER))) + .model(LLMModelDetails.create().name("model-name"))))); + } } 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 e8456da09..a10f24cea 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 @@ -1,6 +1,7 @@ package com.sap.ai.sdk.app.services; import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.GEMINI_2_5_FLASH; +import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.GPT_41_NANO; import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.GPT_4O_MINI; import static com.sap.ai.sdk.orchestration.OrchestrationAiModel.Parameter.TEMPERATURE; import static com.sap.ai.sdk.orchestration.OrchestrationEmbeddingModel.TEXT_EMBEDDING_3_SMALL; @@ -17,11 +18,13 @@ import com.sap.ai.sdk.orchestration.OrchestrationChatResponse; import com.sap.ai.sdk.orchestration.OrchestrationClient; import com.sap.ai.sdk.orchestration.OrchestrationClientException; +import com.sap.ai.sdk.orchestration.OrchestrationConfigReference; import com.sap.ai.sdk.orchestration.OrchestrationEmbeddingRequest; import com.sap.ai.sdk.orchestration.OrchestrationEmbeddingResponse; import com.sap.ai.sdk.orchestration.OrchestrationModuleConfig; import com.sap.ai.sdk.orchestration.OrchestrationPrompt; import com.sap.ai.sdk.orchestration.ResponseJsonSchema; +import com.sap.ai.sdk.orchestration.SystemMessage; import com.sap.ai.sdk.orchestration.TemplateConfig; import com.sap.ai.sdk.orchestration.TranslationConfig; import com.sap.ai.sdk.orchestration.model.DPIEntities; @@ -33,6 +36,15 @@ import com.sap.ai.sdk.orchestration.model.SearchDocumentKeyValueListPair; import com.sap.ai.sdk.orchestration.model.SearchSelectOptionEnum; import com.sap.ai.sdk.orchestration.model.Template; +import com.sap.ai.sdk.prompt.registry.OrchestrationConfigClient; +import com.sap.ai.sdk.prompt.registry.model.LLMModelDetails; +import com.sap.ai.sdk.prompt.registry.model.ModuleConfigs; +import com.sap.ai.sdk.prompt.registry.model.OrchestrationConfig; +import com.sap.ai.sdk.prompt.registry.model.OrchestrationConfigModules; +import com.sap.ai.sdk.prompt.registry.model.OrchestrationConfigPostRequest; +import com.sap.ai.sdk.prompt.registry.model.PromptTemplatingModuleConfig; +import com.sap.ai.sdk.prompt.registry.model.UserChatMessage; +import com.sap.ai.sdk.prompt.registry.model.UserChatMessageContent; import java.io.IOException; import java.util.List; import java.util.Map; @@ -645,4 +657,64 @@ public OrchestrationEmbeddingResponse embed(@Nonnull final List texts) { .withMasking(masking); return client.embed(request); } + + /** + * Chat request to an LLM through the Orchestration service using a template from the prompt + * registry identified by a reference. + * + * @return the assistant response object + */ + @Nonnull + public OrchestrationChatResponse executeConfigFromReference() { + val scenario = "sdk-test-paraphrase"; + val name = "create-3-paraphrases-of-sentence"; + ensureOrchestrationConfigExists(scenario, name); + final List history = List.of(new SystemMessage("Start every sentence with an emoji.")); + final var params = Map.of("phrase", "Hello World"); + final var testReference = + OrchestrationConfigReference.fromScenario("sdk-test-scenario") + .name("test-config-for-OrchestrationTest") + .version("0.0.1") + .messageHistory(history) + .templateParameters(params); + return client.chatCompletionUsingReference(testReference); + } + + private void ensureOrchestrationConfigExists(final String scenario, final String name) { + final OrchestrationConfigClient orchConfigClient = new OrchestrationConfigClient(); + if (!orchConfigExists("test-config-for-OrchestrationTest", orchConfigClient)) { + final OrchestrationConfigPostRequest postRequest = + OrchestrationConfigPostRequest.create() + .name(name) + .version("0.0.1") + .scenario(scenario) + .spec(buildOrchestrationConfig()); + orchConfigClient.createUpdateOrchestrationConfig(postRequest); + } + } + + private boolean orchConfigExists( + final String configName, final OrchestrationConfigClient orchConfigClient) { + return orchConfigClient.listOrchestrationConfigs().getResources().stream() + .anyMatch(resp -> resp.getName().equals(configName)); + } + + private OrchestrationConfig buildOrchestrationConfig() { + return OrchestrationConfig.create() + .modules( + OrchestrationConfigModules.createInnerModuleConfigs( + ModuleConfigs.create() + .promptTemplating( + PromptTemplatingModuleConfig.create() + .prompt( + com.sap.ai.sdk.prompt.registry.model.Template.create() + .template( + UserChatMessage.create() + .content( + new UserChatMessageContent.InnerString( + "Create {{?number}} paraphrases of {{?phrase}}")) + .role(UserChatMessage.RoleEnum.USER)) + .defaults(Map.of("number", "3"))) + .model(LLMModelDetails.create().name(GPT_41_NANO.getName()))))); + } } diff --git a/sample-code/spring-app/src/main/resources/static/index.html b/sample-code/spring-app/src/main/resources/static/index.html index 5393c1804..491c7f912 100644 --- a/sample-code/spring-app/src/main/resources/static/index.html +++ b/sample-code/spring-app/src/main/resources/static/index.html @@ -503,7 +503,7 @@ Orchestration - Template reference + Template and Config Reference @@ -547,6 +547,18 @@ Orchestration + + + + /orchestration/configFromRegistry + + + Chat request to an LLM using an Orchestration config stored in prompt registry. + + + @@ -1108,6 +1120,18 @@ 📚 Prompt Registry + + + + /prompt-registry/listOrchConfigs + + + List all Orchestration Configs. + + + 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 6c805ff60..8fd348930 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 @@ -521,4 +521,11 @@ void wrongModelVersion() { .hasMessageContaining("400") .hasMessageContaining("Model gpt-5 in version wrong-version not found."); } + + @Test + void testExecuteRequestFromReference() { + val result = service.executeConfigFromReference(); + val choices = (result.getOriginalResponse().getFinalResult()).getChoices(); + assertThat(choices.get(0).getMessage().getContent()).isNotEmpty(); + } } diff --git a/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/PromptRegistryTest.java b/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/PromptRegistryTest.java index 94ef5a107..764592221 100644 --- a/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/PromptRegistryTest.java +++ b/sample-code/spring-app/src/test/java/com/sap/ai/sdk/app/controllers/PromptRegistryTest.java @@ -4,6 +4,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.sap.ai.sdk.prompt.registry.model.OrchestrationConfigDeleteResponse; +import com.sap.ai.sdk.prompt.registry.model.OrchestrationConfigPostResponse; import com.sap.ai.sdk.prompt.registry.model.PromptTemplate; import com.sap.ai.sdk.prompt.registry.model.PromptTemplateDeleteResponse; import com.sap.ai.sdk.prompt.registry.model.PromptTemplateListResponse; @@ -103,4 +105,28 @@ void promptRegistryToSpringAi() { assertThat(ChatResponse).isNotNull(); assertThat(ChatResponse.getOutput().getText()).contains("Sports"); } + + @Test + void listOrchestrationConfigs() { + var controller = new PromptRegistryController(); + var result = controller.listOrchConfigs(); + assertThat(result.getCount()).isGreaterThan(0); + } + + @Test + void createDeleteOrchestrationConfig() { + var controller = new PromptRegistryController(); + // cleanup + controller.deleteOrchConfig(); + + // create + OrchestrationConfigPostResponse createdConfig = controller.createOrchConfig(); + assertThat(createdConfig.getMessage()).contains("successful"); + assertThat(createdConfig.getName()).contains(PromptRegistryController.NAME); + + // cleanup + List deletedConfig = controller.deleteOrchConfig(); + assertThat(deletedConfig).hasSize(1); + assertThat(deletedConfig.get(0).getMessage()).contains("successful"); + } }
/orchestration/configFromRegistry
/prompt-registry/listOrchConfigs