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
8 changes: 4 additions & 4 deletions orchestration/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@
</scm>
<properties>
<project.rootdir>${project.basedir}/../</project.rootdir>
<coverage.complexity>82%</coverage.complexity>
<coverage.line>94%</coverage.line>
<coverage.instruction>93%</coverage.instruction>
<coverage.branch>75%</coverage.branch>
<coverage.complexity>83%</coverage.complexity>
<coverage.line>95%</coverage.line>
<coverage.instruction>94%</coverage.instruction>
<coverage.branch>77%</coverage.branch>
<coverage.method>95%</coverage.method>
<coverage.class>100%</coverage.class>
</properties>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,40 @@
import com.sap.ai.sdk.orchestration.model.ChatMessage;
import com.sap.ai.sdk.orchestration.model.ChatMessageContent;
import com.sap.ai.sdk.orchestration.model.MessageToolCall;
import com.sap.ai.sdk.orchestration.model.ReasoningBlock;
import com.sap.ai.sdk.orchestration.model.TextContent;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import lombok.experimental.Accessors;
import lombok.val;

/** Represents a chat message as 'assistant' to the orchestration service. */
@Value
@Getter
@Accessors(fluent = true)
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class AssistantMessage implements Message {

/** The role of the assistant. */
@Nonnull String role = "assistant";

/** The content of the message. */
@Nonnull @Getter MessageContent content;
@Nonnull MessageContent content;

/** Tool call if there is any. */
@Nullable List<MessageToolCall> toolCalls;

/** Reasoning (thinking) blocks from the model's previous turn. */
@SuppressWarnings("PMD.PublicApiExposesModelType")
@Getter(AccessLevel.NONE)
@Nullable
List<ReasoningBlock> reasoningContent;

/**
* Creates a new assistant message with the given single message.
*
Expand All @@ -39,6 +48,7 @@ public class AssistantMessage implements Message {
public AssistantMessage(@Nonnull final String singleMessage) {
content = new MessageContent(List.of(new TextItem(singleMessage)));
toolCalls = null;
reasoningContent = null;
}

/**
Expand All @@ -49,6 +59,7 @@ public AssistantMessage(@Nonnull final String singleMessage) {
AssistantMessage(@Nonnull final MessageContent content) {
this.content = content;
toolCalls = null;
reasoningContent = null;
}

/**
Expand All @@ -61,12 +72,7 @@ public AssistantMessage(@Nonnull final String singleMessage) {
public AssistantMessage(@Nonnull final List<MessageToolCall> toolCalls) {
content = new MessageContent(List.of());
this.toolCalls = toolCalls;
}

private AssistantMessage(
@Nonnull final MessageContent content, @Nullable final List<MessageToolCall> toolCalls) {
this.content = content;
this.toolCalls = toolCalls;
reasoningContent = null;
}

/**
Expand All @@ -81,7 +87,18 @@ private AssistantMessage(
public AssistantMessage withToolCalls(@Nonnull final List<MessageToolCall> toolCalls) {
val newToolcalls = new ArrayList<>(this.toolCalls != null ? this.toolCalls : List.of());
newToolcalls.addAll(toolCalls);
return new AssistantMessage(this.content, newToolcalls);
return new AssistantMessage(this.content, newToolcalls, this.reasoningContent);
}

/**
* Returns a new AssistantMessage instance carrying the given reasoning blocks.
*
* @param reasoningContent the reasoning blocks from the previous assistant turn.
* @return a new AssistantMessage instance with the reasoning content set.
*/
@Nonnull
AssistantMessage withReasoningContent(@Nonnull final List<ReasoningBlock> reasoningContent) {
return new AssistantMessage(this.content, this.toolCalls, List.copyOf(reasoningContent));
Comment thread
n-o-u-r-h-a-n marked this conversation as resolved.
}

@Nonnull
Expand All @@ -93,6 +110,10 @@ public ChatMessage createChatMessage() {
assistantChatMessage.setToolCalls(toolCalls);
}

if (reasoningContent != null) {
assistantChatMessage.setReasoningContent(reasoningContent);
}

ChatMessageContent text;
if (content.items().size() == 1 && content.items().get(0) instanceof TextItem textItem) {
text = ChatMessageContent.create(textItem.text());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,19 @@ public <ValueT> OrchestrationAiModel withParam(
return withParam(param.getName(), value);
}

/**
* Set the {@code reasoning_effort} parameter on this model.
*
* @param effort the reasoning effort level.
* @return a new model with the {@code reasoning_effort} parameter set.
* @see <a href="https://help.sap.com/docs/sap-ai-core/generative-ai/reasoning">SAP AI Core:
* Orchestration - Reasoning</a>
*/
@Nonnull
public OrchestrationAiModel withReasoningEffort(@Nonnull final ReasoningEffort effort) {
return withParam(Parameter.REASONING_EFFORT, effort.getValue());
}

/**
* Parameter key for a model.
*
Expand All @@ -543,6 +556,9 @@ public interface Parameter<ValueT> {
/** The number of chat completion choices to generate for each input message. */
Parameter<Integer> N = () -> "n";

/** The reasoning effort for reasoning-capable models. */
Parameter<String> REASONING_EFFORT = () -> "reasoning_effort";

/**
* The name of the parameter.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.sap.ai.sdk.core.common.StreamedDelta;
import com.sap.ai.sdk.orchestration.model.CompletionPostResponseStreaming;
import com.sap.ai.sdk.orchestration.model.ReasoningBlock;
import java.util.List;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import lombok.val;
Expand All @@ -27,6 +29,24 @@ public String getDeltaContent() {
return "";
}

/**
* Get the reasoning (thinking) text carried by this chunk.
*
* @return the reasoning text of each block in this chunk; never {@code null}, may be empty.
* @see <a href="https://help.sap.com/docs/sap-ai-core/generative-ai/reasoning">SAP AI Core:
* Orchestration - Reasoning</a>
*/
@Nonnull
public List<String> getDeltaReasoningContent() {
val choices = getFinalResult().getChoices();
if (!choices.isEmpty() && choices.get(0).getIndex() == 0) {
return choices.get(0).getDelta().getReasoningContent().stream()
.map(ReasoningBlock::getContent)
.toList();
}
return List.of();
}

@Nullable
@Override
public String getFinishReason() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
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.ReasoningBlock;
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 @@ -67,6 +68,30 @@ public TokenUsage getTokenUsage() {
return originalResponse.getFinalResult().getUsage();
}

/**
* Get the reasoning (thinking) content produced by the model, as text, if any.
*
* @return the reasoning text of each block; never {@code null}, may be empty.
* @see <a href="https://help.sap.com/docs/sap-ai-core/generative-ai/reasoning">SAP AI Core:
* Orchestration - Reasoning</a>
*/
@Nonnull
public List<String> getReasoningContent() {
return getChoice().getMessage().getReasoningContent().stream()
.map(ReasoningBlock::getContent)
.toList();
}

/**
* Get the reasoning (thinking) content as a single joined string, for display purposes.
*
* @return the concatenated reasoning text; empty if there is no reasoning.
*/
@Nonnull
public String getReasoningText() {
return String.join("", getReasoningContent());
}

/**
* Get all messages. This can be used for subsequent prompts as a message history.
*
Expand All @@ -80,13 +105,19 @@ public List<Message> getAllMessages() throws IllegalArgumentException {
originalResponse.getIntermediateResults().getTemplating()) {
if (chatMessage instanceof AssistantChatMessage assistantChatMessage) {
val toolCalls = assistantChatMessage.getToolCalls();
AssistantMessage assistantMessage;
if (!toolCalls.isEmpty()) {
messages.add(new AssistantMessage(toolCalls));
assistantMessage = new AssistantMessage(toolCalls);
} else {
messages.add(
assistantMessage =
new AssistantMessage(
MessageContent.fromChatMessageContent(assistantChatMessage.getContent())));
MessageContent.fromChatMessageContent(assistantChatMessage.getContent()));
}
final var reasoning = assistantChatMessage.getReasoningContent();
if (!reasoning.isEmpty()) {
assistantMessage = assistantMessage.withReasoningContent(reasoning);
}
messages.add(assistantMessage);
} else if (chatMessage instanceof SystemChatMessage systemChatMessage) {
messages.add(
new SystemMessage(
Expand All @@ -106,7 +137,12 @@ public List<Message> getAllMessages() throws IllegalArgumentException {
}
}

messages.add(Message.assistant(getChoice().getMessage().getContent()));
var lastAssistant = Message.assistant(getChoice().getMessage().getContent());
final var lastReasoning = getChoice().getMessage().getReasoningContent();
if (!lastReasoning.isEmpty()) {
lastAssistant = lastAssistant.withReasoningContent(lastReasoning);
}
messages.add(lastAssistant);
return messages;
}

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

import com.sap.ai.sdk.orchestration.model.ReasoningBlock;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nonnull;

/**
* Accumulates reasoning ({@code reasoning_content}) text across the chunks of a streaming
* orchestration response, merging the pieces of each block by their position.
*
* @see <a href="https://help.sap.com/docs/sap-ai-core/generative-ai/reasoning">SAP AI Core:
* Orchestration - Reasoning</a>
*/
public class ReasoningAccumulator {

private final List<StringBuilder> contentByIndex = new ArrayList<>();

/**
* Consume the reasoning content carried by a single stream chunk.
*
* @param delta the streaming delta.
*/
public void accept(@Nonnull final OrchestrationChatCompletionDelta delta) {
final List<ReasoningBlock> chunk = rawReasoning(delta);
for (int i = 0; i < chunk.size(); i++) {
while (contentByIndex.size() <= i) {
contentByIndex.add(new StringBuilder());
}
final String content = chunk.get(i).getContent();
if (!content.isEmpty()) {
contentByIndex.get(i).append(content);
}
}
}

/**
* Assemble the fully accumulated reasoning text, one entry per reasoning block.
*
* @return the assembled reasoning text, in the order it appeared in the stream.
*/
@Nonnull
public List<String> assemble() {
return contentByIndex.stream().map(StringBuilder::toString).toList();
}

@Nonnull
private static List<ReasoningBlock> rawReasoning(
@Nonnull final OrchestrationChatCompletionDelta delta) {
final var choices = delta.getFinalResult().getChoices();
if (!choices.isEmpty() && choices.get(0).getIndex() == 0) {
return choices.get(0).getDelta().getReasoningContent();
}
return List.of();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.sap.ai.sdk.orchestration;

import javax.annotation.Nonnull;

/**
* Reasoning effort levels for the harmonized {@code reasoning_effort} model parameter.
*
* <p>Not all reasoning-capable models support the full range of values. Where a model does not
* support a given effort, orchestration transparently maps it to the closest equivalent.
*
* @see <a href="https://help.sap.com/docs/sap-ai-core/generative-ai/reasoning">SAP AI Core:
* Orchestration - Reasoning</a>
*/
public enum ReasoningEffort {
/** Minimal reasoning effort. */
MINIMAL("minimal"),
/** Low reasoning effort. */
LOW("low"),
/** Medium reasoning effort. */
MEDIUM("medium"),
/** High reasoning effort. */
HIGH("high"),
/** Disable reasoning when the model supports doing so. */
NONE("none");

private final String value;

ReasoningEffort(@Nonnull final String value) {
this.value = value;
}

/**
* Wire string sent as the value of the {@code reasoning_effort} model parameter.
*
* @return the wire value.
*/
@Nonnull
public String getValue() {
return value;
}
}
Loading