Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
38 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
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
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
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,11 @@
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.List;
import javax.annotation.Nonnull;
import lombok.AllArgsConstructor;
Expand All @@ -15,13 +18,17 @@
/**
* 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
* OpenAiToolCallItem}.
*
* @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";
Expand All @@ -32,24 +39,58 @@ class OpenAiAssistantMessage implements OpenAiMessage {
OpenAiMessageContent content;

/**
* Creates a new assistant message with the given single message.
* 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))));
}

/**
* Retrieves the list of tool calls within the content items in this assistant message.
*
* @return a list of {@link OpenAiToolCallItem} representing the tool calls.
* @since 1.6.0
*/
@Nonnull
public List<OpenAiToolCallItem> getToolCalls() {
return this.content().items().stream()
.filter(item -> item instanceof OpenAiToolCallItem)
.map(item -> (OpenAiToolCallItem) item)
.toList();
}

/**
* Converts the message to a serializable object.
*
* @return the corresponding {@code ChatCompletionRequestAssistantMessage} object.
*/
@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()));

for (final var item : content().items()) {
if (item instanceof OpenAiTextItem textItem) {
message.content(ChatCompletionRequestAssistantMessageContent.create(textItem.text()));
} else if (item instanceof OpenAiFunctionCallItem 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 @@ -9,6 +9,7 @@
import com.sap.ai.sdk.foundationmodels.openai.generated.model.CreateChatCompletionRequestAllOfResponseFormat;
import com.sap.ai.sdk.foundationmodels.openai.generated.model.CreateChatCompletionRequestAllOfStop;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
Expand Down Expand Up @@ -149,8 +150,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),
Collections.unmodifiableList(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.ArrayList;
import java.util.Objects;
import javax.annotation.Nonnull;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -61,4 +62,33 @@ public String getContent() {

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

/**
* Gets the {@code OpenAiAssistantMessage} for the first choice in response. *
*
* @return the assistant message
* @since 1.6.0
*/
@Nonnull
public OpenAiAssistantMessage getMessage() {

if (getChoice().getMessage().getToolCalls() == null) {
return OpenAiMessage.assistant(getContent());
}

final var contentItems = new ArrayList<OpenAiContentItem>();
if (!getContent().isEmpty()) {
contentItems.add(new OpenAiTextItem(getContent()));
}

for (final var toolCall : getChoice().getMessage().getToolCalls()) {
contentItems.add(
new OpenAiFunctionCallItem(
toolCall.getId(),
toolCall.getFunction().getName(),
toolCall.getFunction().getArguments()));
}

return new OpenAiAssistantMessage(new OpenAiMessageContent(contentItems));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@
* @since 1.4.0
*/
@Beta
public sealed interface OpenAiContentItem permits OpenAiTextItem, OpenAiImageItem {}
public sealed interface OpenAiContentItem
permits OpenAiTextItem, OpenAiImageItem, OpenAiToolCallItem {}
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 call suggested by an OpenAI model.
*
* @since 1.6.0
*/
@Beta
@Value
@AllArgsConstructor(access = lombok.AccessLevel.PACKAGE)
public class OpenAiFunctionCallItem implements OpenAiToolCallItem {
/** 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
@@ -0,0 +1,9 @@
package com.sap.ai.sdk.foundationmodels.openai;

/**
* Represents a tool call suggested by an OpenAI model.
*
* @since 1.6.0
*/
public sealed interface OpenAiToolCallItem extends OpenAiContentItem
permits OpenAiFunctionCallItem {}
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(new OpenAiFunctionCallItem("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,35 @@ void throwOnSystemMessageWithImage() {
.hasMessageContaining(
"Unknown content type for class com.sap.ai.sdk.foundationmodels.openai.OpenAiImageItem messages.");
}

@Test
void assistantMessageGetToolCalls() {
var message =
new OpenAiAssistantMessage(
new OpenAiMessageContent(
List.of(
new OpenAiTextItem("text"),
new OpenAiFunctionCallItem("id1", "name1", "arguments1"),
new OpenAiFunctionCallItem("id2", "name2", "arguments2"))));

var toolCalls = message.getToolCalls();
assertThat(toolCalls).hasSize(2);

var functionCallItem1 = (OpenAiFunctionCallItem) toolCalls.get(0);
assertThat(functionCallItem1.getId()).isEqualTo("id1");
assertThat(functionCallItem1.getName()).isEqualTo("name1");
assertThat(functionCallItem1.getArguments()).isEqualTo("arguments1");

var functionCallItem2 = (OpenAiFunctionCallItem) toolCalls.get(1);
assertThat(functionCallItem2.getId()).isEqualTo("id2");
assertThat(functionCallItem2.getName()).isEqualTo("name2");
assertThat(functionCallItem2.getArguments()).isEqualTo("arguments2");
}

@ParameterizedTest
@MethodSource("provideValidTextMessageByRole")
void verifyAllMessageTypesMappedToDto(OpenAiMessage message) {
assertThatNoException()
.isThrownBy(() -> OpenAiUtils.createChatCompletionRequestMessage(message));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -480,4 +480,42 @@ void chatCompletionTool() {
}
""")));
}

@Test
void chatCompletionResponseGetMessage() {
stubForChatCompletion();

final var response = client.chatCompletion(new OpenAiChatCompletionRequest("Some text"));
final var simpleMessage = response.getMessage();

assertThat(simpleMessage).isNotNull();
assertThat(simpleMessage).isInstanceOf(OpenAiAssistantMessage.class);
assertThat(simpleMessage.getToolCalls()).isEmpty();

stubForChatCompletionTool();

final var responseWithToolCall =
client.chatCompletion(new OpenAiChatCompletionRequest("Some tool request"));
OpenAiAssistantMessage messageWithToolCall = responseWithToolCall.getMessage();

assertThat(messageWithToolCall).isNotNull();
assertThat(messageWithToolCall).isInstanceOf(OpenAiAssistantMessage.class);
assertThat(messageWithToolCall.content().items()).hasSize(1);
OpenAiFunctionCallItem functionCallItem =
(OpenAiFunctionCallItem) messageWithToolCall.getToolCalls().get(0);
assertThat(functionCallItem.getId()).isEqualTo("call_CUYGJf2j7FRWJMHT3PN3aGxK");
assertThat(functionCallItem.getName()).isEqualTo("fibonacci");
assertThat(functionCallItem.getArguments()).isEqualTo("{\"N\":12}");

// case: both content and tool calls are present
responseWithToolCall.getChoice().getMessage().content("Some content");
var messageWithToolCallsAndContent = responseWithToolCall.getMessage();

assertThat(messageWithToolCallsAndContent).isNotNull();
assertThat(messageWithToolCallsAndContent.content().items()).hasSize(2);
assertThat(messageWithToolCallsAndContent.content().items().get(0))
.isInstanceOf(OpenAiTextItem.class);
assertThat(messageWithToolCallsAndContent.content().items().get(1))
.isInstanceOf(OpenAiFunctionCallItem.class);
}
}
28 changes: 28 additions & 0 deletions sample-code/spring-app/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,11 @@
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<scope>compile</scope>
</dependency>
<!-- scope "test" -->
<dependency>
<groupId>org.junit.jupiter</groupId>
Expand All @@ -181,6 +186,15 @@
<groupId>com.sap.cloud.sdk.cloudplatform</groupId>
<artifactId>cloudplatform-connectivity</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-jsonSchema</artifactId>
<version>2.18.2</version>
</dependency>
<dependency>
<groupId>io.vavr</groupId>
<artifactId>vavr</artifactId>
</dependency>
</dependencies>

<build>
Expand Down Expand Up @@ -215,6 +229,20 @@
<skipAddThirdParty>true</skipAddThirdParty>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>analyze</id>
<configuration>
<ignoredDependencies>
<ignoredDependency>com.fasterxml.jackson.core:jackson-core</ignoredDependency>
</ignoredDependencies>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Loading