Skip to content

Commit 0d54069

Browse files
authored
feat: [OpenAI] Integrate Messaging Convenience (#326)
* Full openapi generated code for openai module * Fix rearrangement of code * Convenience level separated out * Reflect deprecation of direct string input on client with unit tests * E2e for both new and old apis - controller linked to new openai service only * Increasing test coverage * Test coverage for tool * Fix type * PR changes - Move Jackson object initialization to field declaration - update `streamChatCompletionDeltas` in `NewOpenAiService` to use basic string message - `@deprecated` tag on deprecated `chatCompletion` doc - `@Deprecated` annotation on embedding api in client - `OpenAiController` streamChatCompletionDeltas emits usage * Remove docs on OpenAi controller * Remove docs on OpenAi controller * tag public api with `@since` - make jackson mixin package private * Clean up test code for better separation and reduce repetition * Adapt tool test for better usecase * Remove deprecation annotation and java doc tag * Beta annotation on all new classes and client methods returning/taking unstable classes * Fix changes for new generated model class location * Update @SInCE because of latest release * Convert request and response classes to Value classes * Fix @with equality check for Boolean * Move NewOpenAiService to test - Use Old OpenAiService - Adapt tests * utility class introduced for mapping to low level request message * Remove embedding convenience from PR * Method renaming * Charles suggested changes - Enums over string values in test - missed getContent assertion - replicate history test in old api test - Remove `@Value` from OpenAiChatCompletionDelta * Method renaming and make OpenAiChatCompletionDelta constructor access package private * Method renaming, add utility method for jackson and change constructor visibility - follow createX naming format over toX. - move openai object mapper construction logic to utility method - make OpenAiChatCompletionDelta constructor access package private * Improve one plus list construction * Make new delta test more readable - remove docs for overriding methods * Improve java doc in client and request * Remove the redundant test for usage in low level * Minimum integration of messaging convenience - Add user, assistant and system conv on par with orchestration - Adapt sample app service class for image input (new api) - Adapt toDTO to account for OpenAI gen model architecture * Add tool message class * Clean up test code for better separation and reduce repetition * Improve messaging throw behaviour and test coverage - Throw on unsupported content item - improve code quality user message to DTO mapper - Add unit tests for messaging convenience api * PMD suggestions * Increase test coverage - tool message test * `@since` annotation * Rebase fix and make message class constructors package private * Fix access for createChatCompletionRequestMessage in tool message * Renaming new classes in sample code * PR review suggested changes - Message constructors are package private and chained - Remove unnecessary method level `@since` annotation * PR Suggestions - improve docs - fix toolmessage to include toolCallId - reduce toolmessage to single text - improve error message - include test for immutability in messages * improve javadocs --------- Co-authored-by: Roshin Rajan Panackal <[email protected]>
1 parent 0edd308 commit 0d54069

File tree

12 files changed

+651
-65
lines changed

12 files changed

+651
-65
lines changed

foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiAssistantMessage.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
package com.sap.ai.sdk.foundationmodels.openai;
22

3+
import static lombok.AccessLevel.PACKAGE;
4+
35
import com.google.common.annotations.Beta;
46
import com.sap.ai.sdk.foundationmodels.openai.generated.model.ChatCompletionRequestAssistantMessage;
57
import com.sap.ai.sdk.foundationmodels.openai.generated.model.ChatCompletionRequestAssistantMessageContent;
8+
import java.util.List;
69
import javax.annotation.Nonnull;
10+
import lombok.AllArgsConstructor;
11+
import lombok.Getter;
712
import lombok.Value;
813
import lombok.experimental.Accessors;
914

@@ -15,13 +20,25 @@
1520
@Beta
1621
@Value
1722
@Accessors(fluent = true)
23+
@AllArgsConstructor(access = PACKAGE)
1824
class OpenAiAssistantMessage implements OpenAiMessage {
1925

20-
/** The role of the message. */
26+
/** The role associated with this message. */
2127
@Nonnull String role = "assistant";
2228

2329
/** The content of the message. */
24-
@Nonnull String content;
30+
@Getter(onMethod_ = @Beta)
31+
@Nonnull
32+
OpenAiMessageContent content;
33+
34+
/**
35+
* Creates a new assistant message with the given single message.
36+
*
37+
* @param singleMessage the message.
38+
*/
39+
OpenAiAssistantMessage(@Nonnull final String singleMessage) {
40+
this(new OpenAiMessageContent(List.of(new OpenAiTextItem(singleMessage))));
41+
}
2542

2643
/**
2744
* Converts the message to a serializable object.
@@ -30,8 +47,9 @@ class OpenAiAssistantMessage implements OpenAiMessage {
3047
*/
3148
@Nonnull
3249
ChatCompletionRequestAssistantMessage createChatCompletionRequestMessage() {
50+
final var textItem = (OpenAiTextItem) this.content().items().get(0);
3351
return new ChatCompletionRequestAssistantMessage()
3452
.role(ChatCompletionRequestAssistantMessage.RoleEnum.fromValue(role()))
35-
.content(ChatCompletionRequestAssistantMessageContent.create(content));
53+
.content(ChatCompletionRequestAssistantMessageContent.create(textItem.text()));
3654
}
3755
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.sap.ai.sdk.foundationmodels.openai;
2+
3+
import com.google.common.annotations.Beta;
4+
5+
/**
6+
* Represents an item in a {@link OpenAiMessageContent} object.
7+
*
8+
* @since 1.4.0
9+
*/
10+
@Beta
11+
public sealed interface OpenAiContentItem permits OpenAiTextItem, OpenAiImageItem {}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.sap.ai.sdk.foundationmodels.openai;
2+
3+
import com.google.common.annotations.Beta;
4+
import java.util.Locale;
5+
import javax.annotation.Nonnull;
6+
7+
/**
8+
* Represents an image item in a {@link OpenAiMessageContent} object.
9+
*
10+
* @param imageUrl the URL of the image
11+
* @param detailLevel the detail level of the image (optional)
12+
* @since 1.4.0
13+
*/
14+
@Beta
15+
public record OpenAiImageItem(@Nonnull String imageUrl, @Nonnull DetailLevel detailLevel)
16+
implements OpenAiContentItem {
17+
18+
/**
19+
* Creates a new image item with the given image URL.
20+
*
21+
* @param imageUrl the URL of the image
22+
*/
23+
public OpenAiImageItem(@Nonnull final String imageUrl) {
24+
this(imageUrl, DetailLevel.AUTO);
25+
}
26+
27+
/** The detail level of the image. */
28+
public enum DetailLevel {
29+
/** Low detail level. */
30+
LOW("low"),
31+
/** High detail level. */
32+
HIGH("high"),
33+
/** Automatic detail level. */
34+
AUTO("auto");
35+
36+
private final String level;
37+
38+
/**
39+
* Converts a string to a detail level.
40+
*
41+
* @param str the string to convert
42+
* @return the detail level
43+
*/
44+
@Nonnull
45+
static DetailLevel fromString(@Nonnull final String str) {
46+
return DetailLevel.valueOf(str.toUpperCase(Locale.ENGLISH));
47+
}
48+
49+
/**
50+
* Get the string representation of the DetailLevel
51+
*
52+
* @return the DetailLevel as string
53+
*/
54+
@Nonnull
55+
public String toString() {
56+
return level;
57+
}
58+
59+
DetailLevel(@Nonnull final String level) {
60+
this.level = level;
61+
}
62+
}
63+
}
Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.sap.ai.sdk.foundationmodels.openai;
22

33
import com.google.common.annotations.Beta;
4+
import java.util.List;
45
import javax.annotation.Nonnull;
56

67
/**
@@ -9,38 +10,79 @@
910
* @since 1.4.0
1011
*/
1112
@Beta
12-
public interface OpenAiMessage {
13+
public sealed interface OpenAiMessage
14+
permits OpenAiUserMessage, OpenAiAssistantMessage, OpenAiSystemMessage, OpenAiToolMessage {
1315

1416
/**
1517
* A convenience method to create a user message.
1618
*
17-
* @param msg the message content.
19+
* @param message the message content.
1820
* @return the user message.
1921
*/
2022
@Nonnull
21-
static OpenAiMessage user(@Nonnull final String msg) {
22-
return new OpenAiUserMessage(msg);
23+
static OpenAiUserMessage user(@Nonnull final String message) {
24+
return new OpenAiUserMessage(message);
25+
}
26+
27+
/**
28+
* A convenience method to create a user message containing only an image.
29+
*
30+
* @param openAiImageItem the message content.
31+
* @return the user message.
32+
*/
33+
@Nonnull
34+
static OpenAiUserMessage user(@Nonnull final OpenAiImageItem openAiImageItem) {
35+
return new OpenAiUserMessage(new OpenAiMessageContent(List.of(openAiImageItem)));
2336
}
2437

2538
/**
2639
* A convenience method to create an assistant message.
2740
*
28-
* @param msg the message content.
41+
* @param message the message content.
2942
* @return the assistant message.
3043
*/
3144
@Nonnull
32-
static OpenAiMessage assistant(@Nonnull final String msg) {
33-
return new OpenAiAssistantMessage(msg);
45+
static OpenAiAssistantMessage assistant(@Nonnull final String message) {
46+
return new OpenAiAssistantMessage(message);
3447
}
3548

3649
/**
3750
* A convenience method to create a system message.
3851
*
39-
* @param msg the message content.
52+
* @param message the message content.
4053
* @return the system message.
4154
*/
4255
@Nonnull
43-
static OpenAiMessage system(@Nonnull final String msg) {
44-
return new OpenAiSystemMessage(msg);
56+
static OpenAiSystemMessage system(@Nonnull final String message) {
57+
return new OpenAiSystemMessage(message);
4558
}
59+
60+
/**
61+
* A convenience method to create a tool message.
62+
*
63+
* @param message the message content.
64+
* @param toolCallId identifier of the tool call this message is responding to.
65+
* @return the tool message.
66+
*/
67+
@Nonnull
68+
static OpenAiToolMessage tool(@Nonnull final String message, @Nonnull final String toolCallId) {
69+
return new OpenAiToolMessage(message, toolCallId);
70+
}
71+
72+
/**
73+
* Returns the role associated with the message.
74+
*
75+
* @return the role.
76+
*/
77+
@Nonnull
78+
String role();
79+
80+
/**
81+
* Returns the content of the message.
82+
*
83+
* @return the content.
84+
*/
85+
@Beta
86+
@Nonnull
87+
OpenAiMessageContent content();
4688
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.sap.ai.sdk.foundationmodels.openai;
2+
3+
import com.google.common.annotations.Beta;
4+
import java.util.List;
5+
import javax.annotation.Nonnull;
6+
7+
/**
8+
* Represents the content of a chat message.
9+
*
10+
* @param items a list of the content items
11+
* @since 1.4.0
12+
*/
13+
@Beta
14+
public record OpenAiMessageContent(@Nonnull List<OpenAiContentItem> items) {}
Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
package com.sap.ai.sdk.foundationmodels.openai;
22

3+
import static lombok.AccessLevel.PACKAGE;
4+
35
import com.google.common.annotations.Beta;
6+
import com.sap.ai.sdk.foundationmodels.openai.generated.model.ChatCompletionRequestMessageContentPartText;
47
import com.sap.ai.sdk.foundationmodels.openai.generated.model.ChatCompletionRequestSystemMessage;
58
import com.sap.ai.sdk.foundationmodels.openai.generated.model.ChatCompletionRequestSystemMessageContent;
9+
import com.sap.ai.sdk.foundationmodels.openai.generated.model.ChatCompletionRequestSystemMessageContentPart;
10+
import java.util.LinkedList;
11+
import java.util.List;
612
import javax.annotation.Nonnull;
13+
import lombok.AllArgsConstructor;
14+
import lombok.Getter;
715
import lombok.Value;
816
import lombok.experimental.Accessors;
17+
import lombok.experimental.Tolerate;
918

1019
/**
1120
* Represents a chat message as 'system' to OpenAI service. *
@@ -15,23 +24,70 @@
1524
@Beta
1625
@Value
1726
@Accessors(fluent = true)
18-
class OpenAiSystemMessage implements OpenAiMessage {
27+
@AllArgsConstructor(access = PACKAGE)
28+
public class OpenAiSystemMessage implements OpenAiMessage {
1929

20-
/** The role of the message. */
30+
/** The role associated with this message. */
2131
@Nonnull String role = "system";
2232

2333
/** The content of the message. */
24-
@Nonnull String content;
34+
@Getter(onMethod_ = @Beta)
35+
@Nonnull
36+
OpenAiMessageContent content;
37+
38+
/**
39+
* Creates a new system message from a string.
40+
*
41+
* @param message the first message.
42+
*/
43+
@Tolerate
44+
OpenAiSystemMessage(@Nonnull final String message) {
45+
this(new OpenAiMessageContent(List.of(new OpenAiTextItem(message))));
46+
}
47+
48+
/**
49+
* Add text to the message.
50+
*
51+
* @param message the text to add.
52+
* @return the new message.
53+
*/
54+
@Nonnull
55+
public OpenAiSystemMessage withText(@Nonnull final String message) {
56+
final var contentItems = new LinkedList<>(content.items());
57+
contentItems.add(new OpenAiTextItem(message));
58+
return new OpenAiSystemMessage(new OpenAiMessageContent(contentItems));
59+
}
2560

2661
/**
2762
* Converts the message to a serializable object.
2863
*
2964
* @return the corresponding {@code ChatCompletionRequestSystemMessage} object.
65+
* @throws IllegalArgumentException if the content contains unsupported items.
3066
*/
3167
@Nonnull
32-
ChatCompletionRequestSystemMessage createChatCompletionRequestMessage() {
68+
ChatCompletionRequestSystemMessage createChatCompletionRequestMessage()
69+
throws IllegalArgumentException {
70+
final var itemList = this.content().items();
71+
if (itemList.size() == 1 && itemList.get(0) instanceof OpenAiTextItem textItem) {
72+
return new ChatCompletionRequestSystemMessage()
73+
.role(ChatCompletionRequestSystemMessage.RoleEnum.fromValue(role()))
74+
.content(ChatCompletionRequestSystemMessageContent.create(textItem.text()));
75+
}
76+
77+
final var contentList = new LinkedList<ChatCompletionRequestSystemMessageContentPart>();
78+
for (final OpenAiContentItem item : itemList) {
79+
if (item instanceof OpenAiTextItem textItem) {
80+
contentList.add(
81+
new ChatCompletionRequestMessageContentPartText()
82+
.type(ChatCompletionRequestMessageContentPartText.TypeEnum.TEXT)
83+
.text(textItem.text()));
84+
} else {
85+
final var errorMessage = "Unknown content type for " + item.getClass() + " messages.";
86+
throw new IllegalArgumentException(errorMessage);
87+
}
88+
}
3389
return new ChatCompletionRequestSystemMessage()
3490
.role(ChatCompletionRequestSystemMessage.RoleEnum.fromValue(role()))
35-
.content(ChatCompletionRequestSystemMessageContent.create(content()));
91+
.content(ChatCompletionRequestSystemMessageContent.create(contentList));
3692
}
3793
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.sap.ai.sdk.foundationmodels.openai;
2+
3+
import com.google.common.annotations.Beta;
4+
import javax.annotation.Nonnull;
5+
6+
/**
7+
* Represents a text item in a {@link OpenAiMessageContent} object.
8+
*
9+
* @param text the text of the item
10+
* @since 1.4.0
11+
*/
12+
@Beta
13+
public record OpenAiTextItem(@Nonnull String text) implements OpenAiContentItem {}

0 commit comments

Comments
 (0)