Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
77b6637
Call Orchestration with image and multiString via executeRequestFromJ…
Jonas-Isr Jan 6, 2025
026771b
Merge branch 'main' into orchestration-image-support
Jonas-Isr Jan 14, 2025
09816d2
Work in Progress
Jonas-Isr Jan 21, 2025
54a2c19
Align System- nad AssintantMessage with UserMessage
Jonas-Isr Jan 23, 2025
98ba774
Improve exceptions
Jonas-Isr Jan 23, 2025
75afed1
Improve tests
Jonas-Isr Jan 23, 2025
38f3fd4
Prepare draft
Jonas-Isr Jan 23, 2025
f8ea4d0
- Restrict Multi- and ImageContent to appropriate classes;
Jonas-Isr Jan 27, 2025
cd20ca8
Rename addTextMessages() to addText()
Jonas-Isr Jan 27, 2025
a2fe90f
Remove or hide unneeded constructors, use Message.user() etc. instead
Jonas-Isr Jan 27, 2025
ec899a4
Add simple e2e tests
Jonas-Isr Jan 27, 2025
f173057
Prepare draft
Jonas-Isr Jan 28, 2025
c57bd1d
Small change
Jonas-Isr Jan 28, 2025
0524fd7
Refactor newly introduced classes
Jonas-Isr Jan 28, 2025
60e8120
WIP
Jonas-Isr Jan 30, 2025
3754d9c
add test for base64 image
Jonas-Isr Jan 31, 2025
8ab1514
Small cleanups, no more unnecessary Exceptions
Jonas-Isr Jan 31, 2025
e99d7b9
Small changes: ImageItem.DetailLevel is not mandatory by spec, refact…
Jonas-Isr Jan 31, 2025
7c05d89
Delete MessageContent.toString()
Jonas-Isr Jan 31, 2025
5a72a33
Bit of clean up
Jonas-Isr Jan 31, 2025
6100502
Add/improve javadocs
Jonas-Isr Jan 31, 2025
85d077f
Add/improve javadocs some more
Jonas-Isr Feb 3, 2025
7113283
Add annotations
Jonas-Isr Feb 3, 2025
60b97b0
Add explicit allArgs constructor to ImageItem
Jonas-Isr Feb 3, 2025
fa1add0
Fix codestyle etc.
Jonas-Isr Feb 3, 2025
682794a
Fix order in add methods
Jonas-Isr Feb 3, 2025
b671bc7
Improve unit test
Jonas-Isr Feb 3, 2025
3bd521c
Improve e2e test
Jonas-Isr Feb 3, 2025
a152c2f
Merge branch 'main' into orchestration-image-support
Jonas-Isr Feb 3, 2025
0f928a8
Small fixes after merge
Jonas-Isr Feb 3, 2025
6ee7290
Add unit test for message construction
Jonas-Isr Feb 3, 2025
57330d4
Fix sample app after merge
Jonas-Isr Feb 4, 2025
65d01c6
Simplify multiMessage unit test
Jonas-Isr Feb 4, 2025
8a0c79d
Formatting
bot-sdk-js Feb 4, 2025
13c0387
Add constructor for multiple strings for UserMessage and MessageContent
Jonas-Isr Feb 4, 2025
f95c4d3
Small changes
Jonas-Isr Feb 4, 2025
eaf2632
Merge remote-tracking branch 'origin/orchestration-image-support' int…
Jonas-Isr Feb 4, 2025
be70506
Add documentation and release notes
Jonas-Isr Feb 4, 2025
fb0a89f
Improve documentation
Jonas-Isr Feb 5, 2025
8357b89
Update orchestration/src/main/java/com/sap/ai/sdk/orchestration/Messa…
Jonas-Isr Feb 5, 2025
8d32086
Minor changes
Jonas-Isr Feb 5, 2025
dff4fb8
Make `.content()` @Beta
Jonas-Isr Feb 5, 2025
b0e6701
change method names from `addXYZ()` to `andXYZ()`
Jonas-Isr Feb 5, 2025
29ceee1
Delete unnecessary @Nonnull from ImageItem constructor
Jonas-Isr Feb 6, 2025
d8fd060
Improve tests
Jonas-Isr Feb 6, 2025
bcf0d99
Merge branch 'main' into orchestration-image-support
Jonas-Isr Feb 6, 2025
162cb6b
increase coverage
CharlesDuboisSAP Feb 6, 2025
adf81da
We hate Jacoco
CharlesDuboisSAP Feb 6, 2025
a099ee0
Update docs/release-notes/release_notes.md
Jonas-Isr Feb 6, 2025
4899f36
Simplify logic
Jonas-Isr Feb 6, 2025
837fd66
Small fixes
Jonas-Isr Feb 6, 2025
3dd3085
Reduce and streamline amount of public API
Jonas-Isr Feb 7, 2025
d91ca3b
Rename convenience methods to `withXyz()`
Jonas-Isr Feb 7, 2025
e438330
Simplify code and adapt jacoco coverage
Jonas-Isr Feb 7, 2025
a87fa5d
Small change
Jonas-Isr Feb 7, 2025
27a1033
Add release number to javadocs
Jonas-Isr Feb 7, 2025
b505272
Fit jacoco coverage
Jonas-Isr Feb 7, 2025
72f66b5
Merge branch 'main' into orchestration-image-support
Jonas-Isr Feb 7, 2025
dc69ad1
Rename MessageContent.contentItemList to MessageContent.items
Jonas-Isr Feb 7, 2025
3d8315e
Update docs
Jonas-Isr Feb 7, 2025
cd08312
Merge branch 'main' into orchestration-image-support
Jonas-Isr Feb 10, 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
41 changes: 41 additions & 0 deletions docs/guides/ORCHESTRATION_CHAT_COMPLETION.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,47 @@ try (Stream<String> stream = client.streamChatCompletion(prompt, config)) {
Please find [an example in our Spring Boot application](../../sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java).
It shows the usage of Spring Boot's `ResponseBodyEmitter` to stream the chat completion delta messages to the frontend in real-time.


## Add images and multiple text inputs to a message

It's possible to add images and multiple text inputs to a message.

### Add images to a message

An image can be added to a message as follows.

```java
var message = Message.user("Describe the following image");
var newMessage = message.withImage("https://url.to/image.jpg");
```

You can also construct a message with an image directly, using the `ImageItem` class.

```java
var message = Message.user(new ImageItem("https://url.to/image.jpg"));
```

Some AI models, like GPT 4o, support additionally setting the detail level with which the image is read. This can be set via the `DetailLevel` parameter.

```java
var newMessage = message.withImage("https://url.to/image.jpg", ImageItem.DetailLevel.LOW);
```
Note, that currently only user messages are supported for image attachments.

### Add multiple text inputs to a message

It's also possible to add multiple text inputs to a message. This can be useful for providing additional context to the AI model. You can add additional text inputs as follows.

```java
var message = Message.user("What is chess about?");
var newMessage = message.withText("Answer in two sentences.");
```

Note, that only user and system messages are supported for multiple text inputs.

Please find [an example in our Spring Boot application](../../sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/services/OrchestrationService.java).


## Set model parameters

Change your LLM configuration to add model parameters:
Expand Down
6 changes: 4 additions & 2 deletions docs/release-notes/release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@

### 🔧 Compatibility Notes

-
- `Message.content()` returns a `ContentItem` now instead of a `String`. Use `((TextItem) Message.content().items().get(0)).text()` if the corresponding `ContentItem` is a `TextItem` and the string representation is needed.

### ✨ New Functionality

- Upgrade to release 2502a of AI Core.
- [Add Orchestration `LlamaGuardFilter`](../guides/ORCHESTRATION_CHAT_COMPLETION.md#chat-completion-filter).
- Orchestration:
- [Add `LlamaGuardFilter`](../guides/ORCHESTRATION_CHAT_COMPLETION.md#chat-completion-filter).
- [Convenient methods to create messages containing images and multiple text inputs](../guides/ORCHESTRATION_CHAT_COMPLETION.md#add-images-and-multiple-text-inputs-to-a-message)

### 📈 Improvements

Expand Down
8 changes: 4 additions & 4 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>77%</coverage.complexity>
<coverage.complexity>80%</coverage.complexity>
<coverage.line>92%</coverage.line>
<coverage.instruction>92%</coverage.instruction>
<coverage.branch>70%</coverage.branch>
<coverage.method>92%</coverage.method>
<coverage.instruction>93%</coverage.instruction>
<coverage.branch>71%</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
@@ -1,6 +1,9 @@
package com.sap.ai.sdk.orchestration;

import com.google.common.annotations.Beta;
import java.util.List;
import javax.annotation.Nonnull;
import lombok.Getter;
import lombok.Value;
import lombok.experimental.Accessors;

Expand All @@ -13,5 +16,16 @@ public class AssistantMessage implements Message {
@Nonnull String role = "assistant";

/** The content of the message. */
@Nonnull String content;
@Nonnull
@Getter(onMethod_ = @Beta)
MessageContent content;

/**
* Creates a new assistant message with the given single message.
*
* @param singleMessage the single message.
*/
public AssistantMessage(@Nonnull final String singleMessage) {
content = new MessageContent(List.of(new TextItem(singleMessage)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.sap.ai.sdk.orchestration;

/**
* Represents an item in a {@link MessageContent} object.
*
* @since 1.3.0
*/
public sealed interface ContentItem permits TextItem, ImageItem {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.sap.ai.sdk.orchestration;

import java.util.Locale;
import javax.annotation.Nonnull;

/**
* Represents an image item in a {@link MessageContent} object.
*
* @param imageUrl the URL of the image
* @param detailLevel the detail level of the image (optional)
* @since 1.3.0
*/
public record ImageItem(@Nonnull String imageUrl, @Nonnull DetailLevel detailLevel)
implements ContentItem {

/**
* Creates a new image item with the given image URL.
*
* @param imageUrl the URL of the image
* @since 1.3.0
*/
public ImageItem(@Nonnull final String imageUrl) {
this(imageUrl, DetailLevel.AUTO);
}

/**
* The detail level of the image.
*
* @since 1.3.0
*/
public enum DetailLevel {
/** Low detail level. */
LOW("low"),
/** High detail level. */
HIGH("high"),
/** Automatic detail level. */
AUTO("auto");

private final String level;

/**
* Converts a string to a detail level.
*
* @param str the string to convert
* @return the detail level
* @since 1.3.0
*/
@Nonnull
static DetailLevel fromString(@Nonnull final String str) {
return DetailLevel.valueOf(str.toUpperCase(Locale.ENGLISH));
}

/**
* Get the string representation of the DetailLevel
*
* @return the DetailLevel as string
* @since 1.3.0
*/
@Nonnull
public String toString() {
return level;
}

DetailLevel(@Nonnull final String level) {
this.level = level;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,62 @@

import com.google.common.annotations.Beta;
import com.sap.ai.sdk.orchestration.model.ChatMessage;
import com.sap.ai.sdk.orchestration.model.ImageContent;
import com.sap.ai.sdk.orchestration.model.ImageContentImageUrl;
import com.sap.ai.sdk.orchestration.model.MultiChatMessage;
import com.sap.ai.sdk.orchestration.model.MultiChatMessageContent;
import com.sap.ai.sdk.orchestration.model.SingleChatMessage;
import com.sap.ai.sdk.orchestration.model.TextContent;
import java.util.LinkedList;
import java.util.List;
import javax.annotation.Nonnull;

/** Interface representing convenience wrappers of chat message to the orchestration service. */
public sealed interface Message permits UserMessage, AssistantMessage, SystemMessage {

/**
* A convenience method to create a user message.
* A convenience method to create a user message from a string.
*
* @param msg the message content.
* @param message the message content.
* @return the user message.
*/
@Nonnull
static UserMessage user(@Nonnull final String msg) {
return new UserMessage(msg);
static UserMessage user(@Nonnull final String message) {
return new UserMessage(message);
}

/**
* A convenience method to create a user message containing only an image.
*
* @param imageItem the message content.
* @return the user message.
* @since 1.3.0
*/
@Nonnull
static UserMessage user(@Nonnull final ImageItem imageItem) {
return new UserMessage(new MessageContent(List.of(imageItem)));
}

/**
* A convenience method to create an assistant message.
*
* @param msg the message content.
* @param message the message content.
* @return the assistant message.
*/
@Nonnull
static AssistantMessage assistant(@Nonnull final String msg) {
return new AssistantMessage(msg);
static AssistantMessage assistant(@Nonnull final String message) {
return new AssistantMessage(message);
}

/**
* A convenience method to create a system message.
* A convenience method to create a system message from a string.
*
* @param msg the message content.
* @param message the message content.
* @return the system message.
*/
@Nonnull
static SystemMessage system(@Nonnull final String msg) {
return new SystemMessage(msg);
static SystemMessage system(@Nonnull final String message) {
return new SystemMessage(message);
}

/**
Expand All @@ -48,7 +67,21 @@ static SystemMessage system(@Nonnull final String msg) {
*/
@Nonnull
default ChatMessage createChatMessage() {
return SingleChatMessage.create().role(role()).content(content());
final var itemList = this.content().items();
if (itemList.size() == 1 && itemList.get(0) instanceof TextItem textItem) {
return SingleChatMessage.create().role(role()).content(textItem.text());
Copy link
Member Author

Choose a reason for hiding this comment

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

Here we cannot discern between Message.user("text") and Message.user(MessageContent.text("text")), so both get turned into SingleChatMessage. Is that a problem?

Copy link
Member

Choose a reason for hiding this comment

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

Users can use the constructors instead should this ever matter, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

The constructor is protected, but the create() methods etc. are public, so yes.

}
final var contentList = new LinkedList<MultiChatMessageContent>();
for (final ContentItem item : itemList) {
if (item instanceof TextItem textItem) {
contentList.add(TextContent.create().type(TextContent.TypeEnum.TEXT).text(textItem.text()));
} else if (item instanceof ImageItem imageItem) {
final var detail = imageItem.detailLevel().toString();
final var img = ImageContentImageUrl.create().url(imageItem.imageUrl()).detail(detail);
contentList.add(ImageContent.create().type(ImageContent.TypeEnum.IMAGE_URL).imageUrl(img));
}
}
return MultiChatMessage.create().role(role()).content(contentList);
}

/**
Expand All @@ -66,5 +99,5 @@ default ChatMessage createChatMessage() {
*/
@Nonnull
@Beta
String content();
MessageContent content();
Copy link
Member Author

Choose a reason for hiding this comment

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

This is a breaking change. (Note that the corresponding methods in UserMessage etc. were not marked as beta.)

We discussed in the meetings that we do not want to offer a convenience method that returns a string representation of messages anymore. Users can/will use response.getContent() for nearly all use cases and it is not clear how a sensible string representation for messages with many TextItems or IamgeItems should look like.

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

import com.sap.ai.sdk.orchestration.model.ImageContent;
import com.sap.ai.sdk.orchestration.model.MultiChatMessageContent;
import com.sap.ai.sdk.orchestration.model.TextContent;
import java.util.List;
import javax.annotation.Nonnull;

/**
* Represents the content of a chat message.
*
* @param items a list of the content items
* @since 1.3.0
*/
public record MessageContent(@Nonnull List<ContentItem> items) {
@Nonnull
static MessageContent fromMCMContentList(
@Nonnull final List<MultiChatMessageContent> mCMContentList) {
final var itemList =
mCMContentList.stream()
.map(
content -> {
if (content instanceof TextContent text) {
return new TextItem(text.getText());
} else {
final var imageUrl = ((ImageContent) content).getImageUrl();
return (ContentItem)
new ImageItem(
imageUrl.getUrl(),
ImageItem.DetailLevel.fromString(imageUrl.getDetail()));
}
})
.toList();
return new MessageContent(itemList);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
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.MultiChatMessage;
import com.sap.ai.sdk.orchestration.model.SingleChatMessage;
import com.sap.ai.sdk.orchestration.model.TokenUsage;
import java.util.ArrayList;
Expand Down Expand Up @@ -51,33 +52,47 @@ public TokenUsage getTokenUsage() {
/**
* Get all messages. This can be used for subsequent prompts as a message history.
*
* @throws UnsupportedOperationException if the MultiChatMessage type message in chat.
* @throws IllegalArgumentException if the MultiChatMessage type message in chat.
* @return A list of all messages.
*/
@Nonnull
public List<Message> getAllMessages() throws UnsupportedOperationException {
public List<Message> getAllMessages() throws IllegalArgumentException {
final var messages = new ArrayList<Message>();

for (final ChatMessage chatMessage : originalResponse.getModuleResults().getTemplating()) {
if (chatMessage instanceof SingleChatMessage simpleMsg) {
final var message =
switch (simpleMsg.getRole()) {
case "user" -> new UserMessage(simpleMsg.getContent());
case "assistant" -> new AssistantMessage(simpleMsg.getContent());
case "system" -> new SystemMessage(simpleMsg.getContent());
default -> throw new IllegalStateException("Unexpected role: " + simpleMsg.getRole());
};
messages.add(message);
messages.add(chatMessageIntoMessage(simpleMsg));
} else if (chatMessage instanceof MultiChatMessage mCMessage) {
messages.add(chatMessageIntoMessage(mCMessage));
} else {
throw new UnsupportedOperationException(
"Messages of MultiChatMessage type not supported by convenience API");
throw new IllegalArgumentException(
"Messages of type " + chatMessage.getClass() + " are not supported by convenience API");
}
}

messages.add(new AssistantMessage(getChoice().getMessage().getContent()));
messages.add(Message.assistant(getChoice().getMessage().getContent()));
return messages;
}

@Nonnull
private Message chatMessageIntoMessage(@Nonnull final SingleChatMessage simpleMsg) {
return switch (simpleMsg.getRole()) {
case "user" -> Message.user(simpleMsg.getContent());
case "assistant" -> Message.assistant(simpleMsg.getContent());
case "system" -> Message.system(simpleMsg.getContent());
default -> throw new IllegalStateException("Unexpected role: " + simpleMsg.getRole());
};
}

@Nonnull
private Message chatMessageIntoMessage(@Nonnull final MultiChatMessage mCMessage) {
return switch (mCMessage.getRole()) {
case "user" -> new UserMessage(MessageContent.fromMCMContentList(mCMessage.getContent()));
case "system" -> new SystemMessage(MessageContent.fromMCMContentList(mCMessage.getContent()));
default ->
throw new IllegalStateException(
"Unexpected role with complex message: " + mCMessage.getRole());
};
}

/**
* Get the LLM response. Useful for accessing the finish reason or further data like logprobs.
*
Expand Down
Loading