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,109 @@
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.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.14.0
*/
public class OrchestrationConfigClient extends OrchestrationConfigsApi {
Copy link
Contributor

Choose a reason for hiding this comment

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

(Major/Discussion)

I don't think we want to extend a custom class from generated code. Did we do that before? Is there no other way to apply mixins? Otherwise this has a code smell; we're introducing hand-written public API code for the sake of a workaround.


/**
* Instantiates a client to manage Orchestration Configurations on the Prompt Registry service.
*
* @since 1.14.0
*/
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
* @since 1.14.0
*/
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 {}
}
}
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
}
]
}
}
}
5 changes: 3 additions & 2 deletions docs/release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@

### ✨ New Functionality

- [Orchestration] Added new models for `OrchestrationAiModel`: `SAP_ABAP_1`, `SONAR`,`SONAR_PRO`, `GEMINI_2_5_FLASH_LITE`, `CLAUDE_4_5_HAIKU`, `GPT_REALTIME`.
- [Orchestration] Added new models for `OrchestrationAiModel`: `SAP_ABAP_1`, `SONAR`,`SONAR_PRO`, `GEMINI_2_5_FLASH_LITE`, `CLAUDE_4_5_HAIKU`, `GPT_REALTIME`, `COHERE_COMMAND_A_REASONING`, `NOVA_PREMIER`, `COHERE_RERANKER`.
- [Orchestration] Configs stored in prompt registry can now be used for Orchestration calls via reference.
- [Orchestration] Convenience for adding the `metadata_params` option to grounding calls.
- [Orchestration] Added new models for `OrchestrationAiModel`: `COHERE_COMMAND_A_REASONING`, `NOVA_PREMIER`, `COHERE_RERANKER`.
- [Orchestration] Deprecated `DEEPSEEK_R1` model from `OrchestrationAiModel` with no replacement.
- [Prompt Registry] Added support to manage Orchestration configs stored 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,12 @@
package com.sap.ai.sdk.orchestration;

import com.sap.ai.sdk.orchestration.model.ChatMessage;
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 @@ -12,13 +18,17 @@
import com.sap.ai.sdk.orchestration.model.TranslationModuleConfig;
import io.vavr.control.Option;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.annotation.Nonnull;
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 +123,45 @@ static InnerModuleConfigs toModuleConfigs(@Nonnull final OrchestrationModuleConf

return OrchestrationConfigModules.createInnerModuleConfigs(moduleConfig);
}

@Nonnull
static CompletionPostRequest fromReferenceToCompletionPostRequest(
@Nullable final OrchestrationPrompt prompt,
@Nonnull final OrchestrationConfigReference reference) {
List<ChatMessage> messageHistory = List.of();
Map<String, String> placeholders = Map.of();
if (prompt != null) {
if (!prompt.getMessages().isEmpty()) {
log.debug(
"Messages in prompts are ignored when using Orchestration configs via reference. Change the Orchestration config instead.");
}
Comment on lines +134 to +137
Copy link
Member Author

Choose a reason for hiding this comment

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

This could also throw instead of just logging. Was not sure which is better.

messageHistory =
prompt.getMessagesHistory().stream().map(Message::createChatMessage).toList();
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 @@ -25,6 +25,7 @@
import java.util.function.Supplier;
import java.util.stream.Stream;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
Expand Down Expand Up @@ -166,6 +167,25 @@ public CompletionPostResponse executeRequest(@Nonnull final CompletionPostReques
COMPLETION_ENDPOINT, request, CompletionPostResponse.class, customHeaders);
}

/**
* Generate a completion using a referenced Orchestration config and a prompt.
*
* @param prompt The prompt to store placeholder values and the message history
* @param reference A reference to an Orchestration config stored in prompt registry
* @return The completion output
* @since 1.14.0
*/
@Beta
@Nonnull
public OrchestrationChatResponse executeRequestFromReference(
@Nullable final OrchestrationPrompt prompt,
@Nonnull final OrchestrationConfigReference reference) {
Comment on lines +180 to +182
Copy link
Member Author

@Jonas-Isr Jonas-Isr Dec 23, 2025

Choose a reason for hiding this comment

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

We could add

public OrchestrationChatResponse executeRequestFromReference(
      @Nonnull final OrchestrationConfigReference reference) {...}

as well for even more convenience? (The prompt is used to forward placeholder values and the message history, so it is not always needed but probably in most actual use cases).

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not a fan of either method option. Also not liking existing executeRequest siblings.

To me this method looks similar to "normal" use-case

public OrchestrationChatResponse chatCompletion(
@Nonnull final OrchestrationPrompt prompt, @Nonnull final OrchestrationModuleConfig config)

As a user i would assume methods have similar signature whether they use an explicit orchestration config, or an implicit config.

Or is the semantic usage different (enough) to justify a different name schema?

val request =
ConfigToRequestTransformer.fromReferenceToCompletionPostRequest(prompt, 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