-
Notifications
You must be signed in to change notification settings - Fork 16
feat: [Orchestration] Support for Orchestration config persistence #697
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3e34453
8abe277
7039c85
435aac5
7a6d117
e6a8239
33ddc76
54849bd
1576ce5
1b42911
fb1ecd4
67db669
bef51dd
8f9e811
85759ad
cc63701
7082cd0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Question) Why do we need a |
||
|
|
||
| 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 {} | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Question/Major) I'm confused. Wouldn't we want (optional) prompt arguments?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess we expect the usage via prompt-parameters
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All the necessary (and possible) options are now stored in the |
||
| 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 | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.