Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
2 changes: 2 additions & 0 deletions docs/release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@
As a result, the accessors for fields `OrchestrationModuleConfig.inputTranslationConfig` and `OrchestrationModuleConfig.outputTranslationConfig` now handle the implementing class explicitly.
The same applies to helper methods `DpiMasking#createConfig()` and `MaskingProvider#createConfig()`.
- [Orchestration] `OrchestrationTemplate.withTemplate()` has been deprecated. Please use `OrchestrationTemplate.withTemplateMessages()` instead.
- [Orchestration] The method `createConfig()` is removed from `ContentFilter`, `AzureContentFilter` and `LlamaGuardFilter` and is replaced by `createInputFilterConfig()` and `createOutputFilterConfig()`.

### ✨ New Functionality

- [Orchestration] Added support for [transforming a JSON output into an entity](https://sap.github.io/ai-sdk/docs/java/orchestration/chat-completion#json_schema)
- [Orchestration] Added `AzureContentFilter#promptShield()` available for input filtering.

### 📈 Improvements

Expand Down
6 changes: 3 additions & 3 deletions orchestration/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@
</developers>
<properties>
<project.rootdir>${project.basedir}/../</project.rootdir>
<coverage.complexity>83%</coverage.complexity>
<coverage.complexity>82%</coverage.complexity>
<coverage.line>94%</coverage.line>
<coverage.instruction>95%</coverage.instruction>
<coverage.branch>78%</coverage.branch>
<coverage.method>94%</coverage.method>
<coverage.branch>77%</coverage.branch>
<coverage.method>93%</coverage.method>
<coverage.class>100%</coverage.class>
</properties>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.sap.ai.sdk.orchestration;

import com.sap.ai.sdk.orchestration.model.AzureContentSafety;
import com.sap.ai.sdk.orchestration.model.AzureContentSafetyFilterConfig;
import com.sap.ai.sdk.orchestration.model.AzureContentSafetyInput;
import com.sap.ai.sdk.orchestration.model.AzureContentSafetyInputFilterConfig;
import com.sap.ai.sdk.orchestration.model.AzureContentSafetyOutput;
import com.sap.ai.sdk.orchestration.model.AzureContentSafetyOutputFilterConfig;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import lombok.NoArgsConstructor;
Expand Down Expand Up @@ -34,36 +36,68 @@
@Accessors(fluent = true)
public class AzureContentFilter implements ContentFilter {

/* The filter category for hate content. */
/** The filter category for hate content. */
@Nullable AzureFilterThreshold hate;

/* The filter category for self-harm content. */
/** The filter category for self-harm content. */
@Nullable AzureFilterThreshold selfHarm;

/* The filter category for sexual content. */
/** The filter category for sexual content. */
@Nullable AzureFilterThreshold sexual;

/* The filter category for violence content. */
/** The filter category for violence content. */
@Nullable AzureFilterThreshold violence;

/**
* Converts {@code AzureContentFilter} to its serializable counterpart {@link
* AzureContentSafetyFilterConfig}.
* A flag to set prompt shield on input filer.
*
* @return the corresponding {@code AzureContentSafetyFilterConfig} object.
* @since 1.9.0
*/
@Nullable Boolean promptShield;

/**
* Converts {@link AzureContentFilter} to its serializable counterpart {@link
* AzureContentSafetyInputFilterConfig}.
*
* @return the corresponding {@link AzureContentSafetyInputFilterConfig} object.
* @throws IllegalArgumentException if no policies are set.
*/
@Override
@Nonnull
public AzureContentSafetyInputFilterConfig createInputFilterConfig() {
if (hate == null && selfHarm == null && sexual == null && violence == null) {
throw new IllegalArgumentException("At least one filter category must be set");
}

return AzureContentSafetyInputFilterConfig.create()
.type(AzureContentSafetyInputFilterConfig.TypeEnum.AZURE_CONTENT_SAFETY)
.config(
AzureContentSafetyInput.create()
.hate(hate != null ? hate.getAzureThreshold() : null)
.selfHarm(selfHarm != null ? selfHarm.getAzureThreshold() : null)
.sexual(sexual != null ? sexual.getAzureThreshold() : null)
.violence(violence != null ? violence.getAzureThreshold() : null)
.promptShield(promptShield != null ? promptShield : null));
}

/**
* Converts {@link AzureContentFilter} to its serializable counterpart {@link
* AzureContentSafetyOutput}.
*
* @return the corresponding {@link AzureContentSafetyOutputFilterConfig} object.
* @throws IllegalArgumentException if no policies are set.
*/
@Override
@Nonnull
public AzureContentSafetyFilterConfig createConfig() {
public AzureContentSafetyOutputFilterConfig createOutputFilterConfig() {
if (hate == null && selfHarm == null && sexual == null && violence == null) {
throw new IllegalArgumentException("At least one filter category must be set");
}

return AzureContentSafetyFilterConfig.create()
.type(AzureContentSafetyFilterConfig.TypeEnum.AZURE_CONTENT_SAFETY)
return AzureContentSafetyOutputFilterConfig.create()
.type(AzureContentSafetyOutputFilterConfig.TypeEnum.AZURE_CONTENT_SAFETY)
.config(
AzureContentSafety.create()
AzureContentSafetyOutput.create()
.hate(hate != null ? hate.getAzureThreshold() : null)
.selfHarm(selfHarm != null ? selfHarm.getAzureThreshold() : null)
.sexual(sexual != null ? sexual.getAzureThreshold() : null)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.sap.ai.sdk.orchestration;

import com.sap.ai.sdk.orchestration.model.FilterConfig;
import com.sap.ai.sdk.orchestration.model.InputFilterConfig;
import com.sap.ai.sdk.orchestration.model.OutputFilterConfig;
import javax.annotation.Nonnull;

/**
Expand All @@ -17,11 +18,20 @@
public interface ContentFilter {

/**
* A method that produces the serializable equivalent {@link FilterConfig} object from data
* A method that produces the serializable equivalent {@link InputFilterConfig} object from data
* encapsulated in the {@link ContentFilter} object.
*
* @return the corresponding {@code FilterConfig} object.
* @return the corresponding {@link InputFilterConfig} object.
*/
@Nonnull
FilterConfig createConfig();
InputFilterConfig createInputFilterConfig();
Copy link
Contributor

Choose a reason for hiding this comment

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

(Comment)

While I agree to the solution, it doesn't feel great that we're changing public API on convenience layer. That would be considered a breaking change normally. I do understand we only use it as workaround to expose the low-level converter method(s), but still.. 🤔

Copy link
Contributor Author

@CharlesDuboisSAP CharlesDuboisSAP Jul 2, 2025

Choose a reason for hiding this comment

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

I would have blocked the generation entirely until they reverted the spec, but I was on vacation and you accepted the breaking change. I'm still in favor of blocking future changes that don't compile.


/**
* A method that produces the serializable equivalent {@link OutputFilterConfig} object from data
* encapsulated in the {@link ContentFilter} object.
*
* @return the corresponding {@link OutputFilterConfig} object.
*/
@Nonnull
OutputFilterConfig createOutputFilterConfig();
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.sap.ai.sdk.orchestration.model.DPIConfigMaskGroundingInput;
import com.sap.ai.sdk.orchestration.model.DPIEntities;
import com.sap.ai.sdk.orchestration.model.DPIEntityConfig;
import com.sap.ai.sdk.orchestration.model.DPIStandardEntity;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
Expand Down Expand Up @@ -95,7 +96,8 @@ public DpiMasking withAllowList(@Nonnull final List<String> allowList) {
@Nonnull
@Override
public DPIConfig createConfig() {
val entitiesDTO = entities.stream().map(it -> DPIEntityConfig.create().type(it)).toList();
val entitiesDTO =
entities.stream().map(it -> (DPIEntityConfig) DPIStandardEntity.create().type(it)).toList();
return DPIConfig.create()
.type(SAP_DATA_PRIVACY_INTEGRATION)
.method(maskingMethod)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,17 @@
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.sap.ai.sdk.orchestration.model.LLMChoice;
import com.sap.ai.sdk.orchestration.model.LLMModuleResultSynchronous;
import com.sap.ai.sdk.orchestration.model.LLMModuleResult;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
final class JacksonMixins {
/** Mixin to enforce a specific subtype to be deserialized always. */
@JsonTypeInfo(use = JsonTypeInfo.Id.NONE)
@JsonDeserialize(as = LLMModuleResultSynchronous.class)
@JsonDeserialize(as = LLMModuleResult.class)
interface LLMModuleResultMixIn {}

/** Mixin to enforce a specific subtype to be deserialized always. */
@JsonTypeInfo(use = JsonTypeInfo.Id.NONE)
@JsonDeserialize(as = LLMChoice.class)
interface ModuleResultsOutputUnmaskingInnerMixIn {}

@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = As.EXISTING_PROPERTY,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,13 @@ public class LlamaGuardFilter implements ContentFilter {

@Nonnull
@Override
public LlamaGuard38bFilterConfig createConfig() {
public LlamaGuard38bFilterConfig createInputFilterConfig() {
return LlamaGuard38bFilterConfig.create().type(LLAMA_GUARD_3_8B).config(config);
}

@Nonnull
@Override
public LlamaGuard38bFilterConfig createOutputFilterConfig() {
return createInputFilterConfig();
}
}
Original file line number Diff line number Diff line change
@@ -1,45 +1,34 @@
package com.sap.ai.sdk.orchestration;

import com.sap.ai.sdk.core.common.StreamedDelta;
import com.sap.ai.sdk.orchestration.model.CompletionPostResponse;
import com.sap.ai.sdk.orchestration.model.LLMModuleResultSynchronous;
import java.util.Map;
import com.sap.ai.sdk.orchestration.model.CompletionPostResponseStreaming;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import lombok.val;

/** Orchestration chat completion output delta for streaming. */
public class OrchestrationChatCompletionDelta extends CompletionPostResponse
public class OrchestrationChatCompletionDelta extends CompletionPostResponseStreaming
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Accepted breaking change, very minor

implements StreamedDelta {

@Nonnull
@Override
// will be fixed once the generated code add a discriminator which will allow this class to extend
// CompletionPostResponseStreaming
@SuppressWarnings("unchecked")
public String getDeltaContent() {
val choices = ((LLMModuleResultSynchronous) getOrchestrationResult()).getChoices();
val choices = getOrchestrationResult().getChoices();
// Avoid the first delta: "choices":[]
if (!choices.isEmpty()
// Multiple choices are spread out on multiple deltas
// A delta only contains one choice with a variable index
&& choices.get(0).getIndex() == 0) {

final var message = (Map<String, Object>) choices.get(0).toMap().get("delta");
// Avoid the second delta: "choices":[{"delta":{"content":"","role":"assistant"}}]
if (message != null && message.get("content") != null) {
return message.get("content").toString();
}
final var message = choices.get(0).getDelta();
return message.getContent();
}
return "";
}

@Nullable
@Override
public String getFinishReason() {
return ((LLMModuleResultSynchronous) getOrchestrationResult())
.getChoices()
.get(0)
.getFinishReason();
return getOrchestrationResult().getChoices().get(0).getFinishReason();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import com.sap.ai.sdk.orchestration.model.ChatMessageContent;
import com.sap.ai.sdk.orchestration.model.CompletionPostResponse;
import com.sap.ai.sdk.orchestration.model.LLMChoice;
import com.sap.ai.sdk.orchestration.model.LLMModuleResultSynchronous;
import com.sap.ai.sdk.orchestration.model.SystemChatMessage;
import com.sap.ai.sdk.orchestration.model.TokenUsage;
import com.sap.ai.sdk.orchestration.model.ToolChatMessage;
Expand Down Expand Up @@ -53,7 +52,7 @@ public String getContent() throws OrchestrationClientException {
*/
@Nonnull
public TokenUsage getTokenUsage() {
return ((LLMModuleResultSynchronous) originalResponse.getOrchestrationResult()).getUsage();
return originalResponse.getOrchestrationResult().getUsage();
}

/**
Expand Down Expand Up @@ -106,9 +105,7 @@ public List<Message> getAllMessages() throws IllegalArgumentException {
@Nonnull
public LLMChoice getChoice() {
// We expect choices to be defined and never empty.
return ((LLMModuleResultSynchronous) originalResponse.getOrchestrationResult())
.getChoices()
.get(0);
return originalResponse.getOrchestrationResult().getChoices().get(0);
}

/**
Expand All @@ -126,7 +123,8 @@ public LLMChoice getChoice() {
@Nonnull
public <T> T asEntity(@Nonnull final Class<T> type) throws OrchestrationClientException {
final String refusal =
((LLMModuleResultSynchronous) getOriginalResponse().getOrchestrationResult())
getOriginalResponse()
.getOrchestrationResult()
.getChoices()
.get(0)
.getMessage()
Expand Down
Loading