Skip to content
Merged
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
1662ae7
Tool call full e2e unverified
rpanackal Mar 14, 2025
9ef8e6f
CI
rpanackal Mar 14, 2025
ef9bdc4
Reuse tool
rpanackal Mar 17, 2025
fd6acdc
Remove unnecessary line inserts
rpanackal Mar 17, 2025
a6a209b
Create schema with jackson
rpanackal Mar 24, 2025
9ad7225
Merge remote-tracking branch 'refs/remotes/origin/main' into test/ope…
rpanackal Mar 25, 2025
9a5aec3
First version
rpanackal Mar 26, 2025
275eee5
Formatting
bot-sdk-js Mar 26, 2025
ad9ef65
Testing and javadocs
rpanackal Mar 27, 2025
e70eb29
improve test clarity
rpanackal Mar 27, 2025
a13b183
Merge remote-tracking branch 'refs/remotes/origin/main' into test/ope…
rpanackal Mar 27, 2025
03a14a4
Merge branch 'refs/heads/test/openai/tool-call-execute' into feat/ope…
rpanackal Mar 27, 2025
bf2ce11
Remove strict tool invocation config
rpanackal Mar 27, 2025
f74a0a7
Merge branch 'refs/heads/test/openai/tool-call-execute' into feat/ope…
rpanackal Mar 27, 2025
698eab8
Fix maven dependency scope issue
rpanackal Mar 28, 2025
f694f40
Fix maven dependency scope issue + 1
rpanackal Mar 28, 2025
4e388e2
Merge branch 'refs/heads/test/openai/tool-call-execute' into feat/ope…
rpanackal Mar 31, 2025
78aa10e
Refactor. toolCalls get dedicated field in assistant message
rpanackal Mar 31, 2025
49b8be4
minor fixes
rpanackal Mar 31, 2025
78c2909
Merge branch 'refs/heads/test/openai/tool-call-execute' into feat/ope…
rpanackal Mar 31, 2025
05d6218
minor fixes
rpanackal Mar 31, 2025
23a247a
Test message list in request externally unmodifiable
rpanackal Mar 31, 2025
4642de0
Test message list in request externally unmodifiable
rpanackal Mar 31, 2025
96647f5
minor javadoc update
rpanackal Mar 31, 2025
f8ee9d4
Merge remote-tracking branch 'refs/remotes/origin/main' into feat/ope…
rpanackal Mar 31, 2025
bf7d86a
Remove sample app changes
rpanackal Mar 31, 2025
1a6d21a
All e2e test code
rpanackal Mar 31, 2025
26b7dae
@beta annotation
rpanackal Mar 31, 2025
f616263
Update foundation-models/openai/src/main/java/com/sap/ai/sdk/foundati…
rpanackal Apr 1, 2025
cd11f37
Apply suggestions from code review
rpanackal Apr 1, 2025
b95b960
update getMessage method impl readability
rpanackal Apr 1, 2025
9062e3a
Move test to generated client test class
rpanackal Apr 1, 2025
aa04e61
Merge branch 'refs/heads/feat/openai/tool-call-execute' into test/ope…
rpanackal Apr 1, 2025
69b49ea
Extend tests
rpanackal Apr 1, 2025
d26ecbf
Reduce test
rpanackal Apr 1, 2025
40f1e22
Refactor test
rpanackal Apr 1, 2025
bd61ce2
Formatting
bot-sdk-js Apr 1, 2025
d440c28
import statement
rpanackal Apr 1, 2025
c6969f4
Merge branch 'refs/heads/main' into feat/openai/tool-call-execute
rpanackal Apr 1, 2025
a36bef0
Merge branch 'feat/openai/tool-call-execute' of https://github.com/SA…
rpanackal Apr 1, 2025
3e2efdc
Merge branch 'refs/heads/feat/openai/tool-call-execute' into test/ope…
rpanackal Apr 1, 2025
a27294f
refactor: remove unused chatCompletionTools method and
rpanackal Apr 2, 2025
c752e52
pom dependency order and controller method params
rpanackal Apr 3, 2025
750e73b
pom dependency order and controller method params
rpanackal Apr 3, 2025
0e22166
update index file
rpanackal Apr 3, 2025
00f93cc
Merge remote-tracking branch 'refs/remotes/origin/main' into test/ope…
rpanackal Apr 3, 2025
4aa87e6
Improve sample code
rpanackal Apr 3, 2025
ddae356
Move the service class out of test
rpanackal Apr 3, 2025
f1c6c50
PMD
rpanackal Apr 3, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@
import static lombok.AccessLevel.PACKAGE;

import com.google.common.annotations.Beta;
import com.sap.ai.sdk.foundationmodels.openai.generated.model.ChatCompletionMessageToolCall;
import com.sap.ai.sdk.foundationmodels.openai.generated.model.ChatCompletionMessageToolCallFunction;
import com.sap.ai.sdk.foundationmodels.openai.generated.model.ChatCompletionRequestAssistantMessage;
import com.sap.ai.sdk.foundationmodels.openai.generated.model.ChatCompletionRequestAssistantMessageContent;
import com.sap.ai.sdk.foundationmodels.openai.generated.model.ToolCallType;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nonnull;
import lombok.AllArgsConstructor;
Expand All @@ -15,29 +19,48 @@
/**
* Represents a chat message as 'assistant' to OpenAI service.
*
* <p>When {@link OpenAiAssistantMessage} is received from {@link OpenAiChatCompletionResponse}, it
* may contain tool calls that need to be executed. The tool calls are represented as {@link
* OpenAiToolCall}.
*
* @since 1.4.0
*/
@Beta
@Value
@Accessors(fluent = true)
@AllArgsConstructor(access = PACKAGE)
class OpenAiAssistantMessage implements OpenAiMessage {
public class OpenAiAssistantMessage implements OpenAiMessage {

/** The role associated with this message. */
@Nonnull String role = "assistant";

/** The content of the message. */
/**
* The content of the message.
*
* <p>May contain an empty list of {@link OpenAiContentItem} when tool calls are present.
*/
@Getter(onMethod_ = @Beta)
@Nonnull
OpenAiMessageContent content;

/**
* Creates a new assistant message with the given single message.
* The tool calls associated with this message if present.
*
* @since 1.6.0
*/
@Getter(onMethod_ = @Beta)
@Nonnull
List<OpenAiToolCall> toolCalls;

/**
* Creates a new assistant message with the given single message as text content.
*
* @param singleMessage the message.
*/
OpenAiAssistantMessage(@Nonnull final String singleMessage) {
this(new OpenAiMessageContent(List.of(new OpenAiTextItem(singleMessage))));
this(
new OpenAiMessageContent(List.of(new OpenAiTextItem(singleMessage))),
Collections.emptyList());
}

/**
Expand All @@ -47,9 +70,31 @@ class OpenAiAssistantMessage implements OpenAiMessage {
*/
@Nonnull
ChatCompletionRequestAssistantMessage createChatCompletionRequestMessage() {
final var textItem = (OpenAiTextItem) this.content().items().get(0);
return new ChatCompletionRequestAssistantMessage()
.role(ChatCompletionRequestAssistantMessage.RoleEnum.fromValue(role()))
.content(ChatCompletionRequestAssistantMessageContent.create(textItem.text()));
final var message =
new ChatCompletionRequestAssistantMessage()
.role(ChatCompletionRequestAssistantMessage.RoleEnum.fromValue(role()));

final var items = content().items();
if (!items.isEmpty() && items.get(0) instanceof OpenAiTextItem textItem) {
message.content(ChatCompletionRequestAssistantMessageContent.create(textItem.text()));
}

for (final var item : toolCalls()) {
if (item instanceof OpenAiFunctionCall functionItem) {
final var functionCall =
new ChatCompletionMessageToolCallFunction()
.name(functionItem.getName())
.arguments(functionItem.getArguments());

final var toolCall =
new ChatCompletionMessageToolCall()
.type(ToolCallType.FUNCTION)
.id(functionItem.getId())
.function(functionCall);

message.addToolCallsItem(toolCall);
}
}
return message;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,19 @@ public OpenAiChatCompletionRequest(@Nonnull final String message) {
@Tolerate
public OpenAiChatCompletionRequest(
@Nonnull final OpenAiMessage message, @Nonnull final OpenAiMessage... messages) {
this(Lists.asList(message, messages));
}

/**
* Creates an OpenAiChatCompletionPrompt with a list of messages.
*
* @param messages the list of messages to be added to the prompt
* @since 1.6.0
*/
@Tolerate
public OpenAiChatCompletionRequest(@Nonnull final List<OpenAiMessage> messages) {
this(
Lists.asList(message, messages),
List.copyOf(messages),
null,
null,
null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.sap.ai.sdk.foundationmodels.openai.generated.model.CompletionUsage;
import com.sap.ai.sdk.foundationmodels.openai.generated.model.CreateChatCompletionResponse;
import com.sap.ai.sdk.foundationmodels.openai.generated.model.CreateChatCompletionResponseChoicesInner;
import java.util.List;
import java.util.Objects;
import javax.annotation.Nonnull;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -50,6 +51,9 @@ public CreateChatCompletionResponseChoicesInner getChoice() {
/**
* Gets the content of the first choice.
*
* <p>The content may be empty {@code ""} if the assistant did not return any content i.e. when
* tool calls are present.
*
* @return the content of the first choice
* @throws OpenAiClientException if the content is filtered by the content filter
*/
Expand All @@ -61,4 +65,35 @@ public String getContent() {

return Objects.requireNonNullElse(getChoice().getMessage().getContent(), "");
}

/**
* Gets the {@code OpenAiAssistantMessage} for the first choice.
*
* @return the assistant message
* @throws OpenAiClientException if the content is filtered by the content filter
* @since 1.6.0
*/
@Nonnull
public OpenAiAssistantMessage getMessage() {
final var toolCalls = getChoice().getMessage().getToolCalls();

if (toolCalls == null) {
return OpenAiMessage.assistant(getContent());
}

final List<OpenAiContentItem> contentItems =
getContent().isEmpty() ? List.of() : List.of(new OpenAiTextItem(getContent()));

final var openAiToolCalls =
toolCalls.stream()
.<OpenAiToolCall>map(
toolCall ->
new OpenAiFunctionCall(
toolCall.getId(),
toolCall.getFunction().getName(),
toolCall.getFunction().getArguments()))
.toList();

return new OpenAiAssistantMessage(new OpenAiMessageContent(contentItems), openAiToolCalls);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.sap.ai.sdk.foundationmodels.openai;

import com.google.common.annotations.Beta;
import javax.annotation.Nonnull;
import lombok.AllArgsConstructor;
import lombok.Value;

/**
* Represents a function type tool called by an OpenAI model.
*
* @since 1.6.0
*/
@Beta
@Value
@AllArgsConstructor(access = lombok.AccessLevel.PACKAGE)
public class OpenAiFunctionCall implements OpenAiToolCall {
/** The unique identifier for the function call. */
@Nonnull String id;

/** The name of the function to be called. */
@Nonnull String name;

/** The arguments for the function call, encoded as a JSON string. */
@Nonnull String arguments;
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ static OpenAiSystemMessage system(@Nonnull final String message) {
/**
* A convenience method to create a tool message.
*
* @param message the message content.
* @param toolCallId identifier of the tool call this message is responding to.
* @param message response of the executed tool call.
* @param toolCallId identifier of the tool call that the assistant expected.
* @return the tool message.
*/
@Nonnull
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.sap.ai.sdk.foundationmodels.openai;

import com.google.common.annotations.Beta;

/**
* Represents a tool called by an OpenAI model.
*
* @since 1.6.0
*/
@Beta
public sealed interface OpenAiToolCall permits OpenAiFunctionCall {}
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,17 @@ public class OpenAiToolMessage implements OpenAiMessage {
/** The role associated with this message. */
@Nonnull String role = "tool";

/** The content of the message. */
/** Response of the executed tool call. */
@Nonnull OpenAiMessageContent content;

/** The tool call id associated with this message. */
@Nonnull private final String toolCallId;

/**
* Creates a new tool message from a string and tool call id.
* Creates a new tool message from a tool execution response and tool call id.
*
* @param message the first message.
* @param toolCallId identifier of the tool call this message is responding to.
* @param message response of the executed tool call.
* @param toolCallId identifier of the tool call that the assistant expected.
*/
OpenAiToolMessage(@Nonnull final String message, @Nonnull final String toolCallId) {
this(new OpenAiMessageContent(List.of(new OpenAiTextItem(message))), toolCallId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ static ChatCompletionRequestMessage createChatCompletionRequestMessage(
return assistantMessage.createChatCompletionRequestMessage();
} else if (message instanceof OpenAiSystemMessage systemMessage) {
return systemMessage.createChatCompletionRequestMessage();
} else if (message instanceof OpenAiToolMessage toolMessage) {
return toolMessage.createChatCompletionRequestMessage();
} else {
throw new IllegalArgumentException("Unknown message type: " + message.getClass());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.sap.ai.sdk.foundationmodels.openai;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNoException;

import com.sap.ai.sdk.foundationmodels.openai.generated.model.ChatCompletionRequestAssistantMessage;
import com.sap.ai.sdk.foundationmodels.openai.generated.model.ChatCompletionRequestAssistantMessageContent;
Expand All @@ -13,6 +14,7 @@
import com.sap.ai.sdk.foundationmodels.openai.generated.model.ChatCompletionRequestToolMessageContent;
import com.sap.ai.sdk.foundationmodels.openai.generated.model.ChatCompletionRequestUserMessage;
import com.sap.ai.sdk.foundationmodels.openai.generated.model.ChatCompletionRequestUserMessageContent;
import com.sap.ai.sdk.foundationmodels.openai.generated.model.ToolCallType;
import java.net.URI;
import java.util.List;
import java.util.stream.Stream;
Expand Down Expand Up @@ -194,6 +196,22 @@ void assistantMessageToDto() {
((ChatCompletionRequestAssistantMessageContent.InnerString) requestMessage.getContent())
.value())
.isEqualTo(validText);

var messageWithFunctionCall =
new OpenAiAssistantMessage(
new OpenAiMessageContent(List.of()),
List.of(new OpenAiFunctionCall("id", "name", "arguments")));
var requestMessageWithFunctionCall =
messageWithFunctionCall.createChatCompletionRequestMessage();

assertThat(requestMessageWithFunctionCall.getToolCalls()).hasSize(1);
assertThat(requestMessageWithFunctionCall.getToolCalls().get(0).getType())
.isEqualTo(ToolCallType.FUNCTION);
assertThat(requestMessageWithFunctionCall.getToolCalls().get(0).getId()).isEqualTo("id");
assertThat(requestMessageWithFunctionCall.getToolCalls().get(0).getFunction().getName())
.isEqualTo("name");
assertThat(requestMessageWithFunctionCall.getToolCalls().get(0).getFunction().getArguments())
.isEqualTo("arguments");
}

@Test
Expand All @@ -220,4 +238,11 @@ void throwOnSystemMessageWithImage() {
.hasMessageContaining(
"Unknown content type for class com.sap.ai.sdk.foundationmodels.openai.OpenAiImageItem messages.");
}

@ParameterizedTest
@MethodSource("provideValidTextMessageByRole")
void testCreateChatCompletionRequestMessage(OpenAiMessage message) {
assertThatNoException()
.isThrownBy(() -> OpenAiUtils.createChatCompletionRequestMessage(message));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.sap.ai.sdk.foundationmodels.openai.generated.model.ChatCompletionToolChoiceOption;
import com.sap.ai.sdk.foundationmodels.openai.generated.model.CreateChatCompletionRequestAllOfStop;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -94,4 +95,23 @@ void withToolChoiceFunction() {
assertThat(choice.getType().getValue()).isEqualTo("function");
assertThat(choice.getFunction().getName()).isEqualTo("functionName");
}

@Test
void messageListExternallyUnmodifiable() {
var originalList = new ArrayList<OpenAiMessage>();
OpenAiUserMessage user = OpenAiMessage.user("Initial message");
originalList.add(user);

var request = new OpenAiChatCompletionRequest(originalList);

var generatedRequest = request.createCreateChatCompletionRequest();
assertThat(generatedRequest.getMessages()).hasSize(1);

originalList.add(OpenAiMessage.user("Another message"));

var generatedRequestAfter = request.createCreateChatCompletionRequest();
assertThat(generatedRequestAfter.getMessages())
.as("Modifying the original list should not affect the messages in the request object.")
.hasSize(1);
}
}
Loading