Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions core-services/prompt-registry/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@
</scm>
<properties>
<project.rootdir>${project.basedir}/../../</project.rootdir>
<coverage.complexity>73%</coverage.complexity>
<coverage.line>87%</coverage.line>
<coverage.instruction>89%</coverage.instruction>
<coverage.branch>75%</coverage.branch>
<coverage.method>75%</coverage.method>
<coverage.complexity>85%</coverage.complexity>
<coverage.line>91%</coverage.line>
<coverage.instruction>93%</coverage.instruction>
<coverage.branch>100%</coverage.branch>
<coverage.method>81%</coverage.method>
<coverage.class>100%</coverage.class>
</properties>

Expand Down Expand Up @@ -97,6 +97,14 @@
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<!-- scope "provided" -->
<dependency>
<groupId>org.projectlombok</groupId>
Expand Down
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));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Question)

Why do we need a MappingJackson2YamlHttpMessageConverter?


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
}
]
}
}
}
3 changes: 2 additions & 1 deletion docs/release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 4 additions & 4 deletions orchestration/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@
</scm>
<properties>
<project.rootdir>${project.basedir}/../</project.rootdir>
<coverage.complexity>80%</coverage.complexity>
<coverage.line>94%</coverage.line>
<coverage.complexity>82%</coverage.complexity>
<coverage.line>95%</coverage.line>
<coverage.instruction>93%</coverage.instruction>
<coverage.branch>74%</coverage.branch>
<coverage.method>93%</coverage.method>
<coverage.branch>75%</coverage.branch>
<coverage.method>94%</coverage.method>
<coverage.class>100%</coverage.class>
</properties>

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Question/Major)

I'm confused. Wouldn't we want (optional) prompt arguments?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we expect the usage via prompt-parameters

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the necessary (and possible) options are now stored in the OrchestrationConfigReference object.

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
Expand Down
Loading