Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ public OpenAiClient withApiVersion(@Nonnull final String apiVersion) {
* @see AiCoreService#getInferenceDestination(String)
* @return a new OpenAI client.
*/
@Beta
@Nonnull
public static OpenAiClient withCustomDestination(@Nonnull final Destination destination) {
final OpenAiClient client = new OpenAiClient(destination);
Expand Down Expand Up @@ -156,7 +155,6 @@ public OpenAiChatCompletionOutput chatCompletion(@Nonnull final String prompt)
* @throws OpenAiClientException if the request fails
* @since 1.4.0
*/
@Beta
@Nonnull
public OpenAiChatCompletionResponse chatCompletion(
@Nonnull final OpenAiChatCompletionRequest request) throws OpenAiClientException {
Expand All @@ -173,7 +171,6 @@ public OpenAiChatCompletionResponse chatCompletion(
* @throws OpenAiClientException if the request fails
* @since 1.4.0
*/
@Beta
@Nonnull
public CreateChatCompletionResponse chatCompletion(
@Nonnull final CreateChatCompletionRequest request) throws OpenAiClientException {
Expand Down Expand Up @@ -274,7 +271,6 @@ private static void throwOnContentFilter(@Nonnull final OpenAiChatCompletionDelt
* @see #streamChatCompletion(String)
* @since 1.4.0
*/
@Beta
@Nonnull
public Stream<OpenAiChatCompletionDelta> streamChatCompletionDeltas(
@Nonnull final OpenAiChatCompletionRequest request) throws OpenAiClientException {
Expand All @@ -291,7 +287,6 @@ public Stream<OpenAiChatCompletionDelta> streamChatCompletionDeltas(
* @see #streamChatCompletionDeltas(OpenAiChatCompletionRequest) for a higher-level API
* @since 1.4.0
*/
@Beta
@Nonnull
public Stream<OpenAiChatCompletionDelta> streamChatCompletionDeltas(
@Nonnull final CreateChatCompletionRequest request) throws OpenAiClientException {
Expand Down Expand Up @@ -358,7 +353,6 @@ private void warnIfUnsupportedUsage() {
* @see #embedding(EmbeddingsCreateRequest) for full confgurability.
* @since 1.4.0
*/
@Beta
@Nonnull
public OpenAiEmbeddingResponse embedding(@Nonnull final OpenAiEmbeddingRequest request)
throws OpenAiClientException {
Expand All @@ -374,7 +368,6 @@ public OpenAiEmbeddingResponse embedding(@Nonnull final OpenAiEmbeddingRequest r
* @see #embedding(OpenAiEmbeddingRequest) for conveninece api
* @since 1.4.0
*/
@Beta
@Nonnull
public EmbeddingsCreate200Response embedding(@Nonnull final EmbeddingsCreateRequest request)
throws OpenAiClientException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sap.ai.sdk.app.services.OpenAiService;
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiUsage;
import com.sap.ai.sdk.foundationmodels.openai.generated.model.CompletionUsage;
import com.sap.cloud.sdk.cloudplatform.thread.ThreadContextExecutors;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicReference;
Expand Down Expand Up @@ -47,14 +47,14 @@ ResponseEntity<ResponseBodyEmitter> streamChatCompletionDeltas() {
final var message = "Can you give me the first 100 numbers of the Fibonacci sequence?";
final var stream = service.streamChatCompletionDeltas(message);
final var emitter = new ResponseBodyEmitter();
final var totalUsage = new AtomicReference<OpenAiUsage>();
final var totalUsage = new AtomicReference<CompletionUsage>();
final Runnable consumeStream =
() -> {
try (stream) {
stream.forEach(
delta -> {
// Instead of getCompletionUsage(MAPPER), we now use getUsage()
final var usage = delta.getUsage();
final var usage = delta.getCompletionUsage();
totalUsage.compareAndExchange(null, usage);
send(emitter, delta.getDeltaContent());
});
Expand Down Expand Up @@ -116,7 +116,7 @@ Object chatCompletionImage(
if ("json".equals(format)) {
return response;
}
return response.getChoices().get(0).getMessage();
return response.getChoice().getMessage();
}

@GetMapping("/chatCompletionToolExecution")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,61 @@
import static com.sap.ai.sdk.foundationmodels.openai.OpenAiModel.GPT_4O;
import static com.sap.ai.sdk.foundationmodels.openai.OpenAiModel.GPT_4O_MINI;
import static com.sap.ai.sdk.foundationmodels.openai.OpenAiModel.TEXT_EMBEDDING_3_SMALL;
import static com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionTool.ToolType.FUNCTION;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.module.jsonSchema.JsonSchemaGenerator;
import com.sap.ai.sdk.core.AiCoreService;
import com.sap.ai.sdk.foundationmodels.openai.OpenAiChatCompletionDelta;
import com.sap.ai.sdk.foundationmodels.openai.OpenAiChatCompletionRequest;
import com.sap.ai.sdk.foundationmodels.openai.OpenAiChatCompletionResponse;
import com.sap.ai.sdk.foundationmodels.openai.OpenAiClient;
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionDelta;
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionFunction;
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionOutput;
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionParameters;
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatCompletionTool;
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatMessage;
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiChatToolCall;
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiEmbeddingOutput;
import com.sap.ai.sdk.foundationmodels.openai.model.OpenAiEmbeddingParameters;
import com.sap.ai.sdk.foundationmodels.openai.OpenAiEmbeddingRequest;
import com.sap.ai.sdk.foundationmodels.openai.OpenAiEmbeddingResponse;
import com.sap.ai.sdk.foundationmodels.openai.OpenAiImageItem;
import com.sap.ai.sdk.foundationmodels.openai.OpenAiMessage;
import com.sap.ai.sdk.foundationmodels.openai.OpenAiTool;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import javax.annotation.Nonnull;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.stereotype.Service;

/** Service class for OpenAI service */
/** Service class for OpenAI service using latest convenience api */
@Service
@Slf4j
public class OpenAiService {
private static final ObjectMapper JACKSON = new ObjectMapper();

/**
* Chat request to OpenAI
*
* @param prompt The prompt to send to the assistant
* @return the assistant message response
*/
@Nonnull
public OpenAiChatCompletionOutput chatCompletion(@Nonnull final String prompt) {
return OpenAiClient.forModel(GPT_4O_MINI).chatCompletion(prompt);
public OpenAiChatCompletionResponse chatCompletion(@Nonnull final String prompt) {
return OpenAiClient.forModel(GPT_4O_MINI)
.chatCompletion(new OpenAiChatCompletionRequest(prompt));
}

/**
* Chat requests to OpenAI and updating the messages history
*
* @param previousMessage The request to send to the assistant
* @return the assistant message response
*/
@Nonnull
public OpenAiChatCompletionResponse messagesHistory(@Nonnull final String previousMessage) {
val messagesList = new ArrayList<OpenAiMessage>();
messagesList.add(OpenAiMessage.user(previousMessage));

final OpenAiChatCompletionResponse result =
OpenAiClient.forModel(GPT_4O_MINI)
.chatCompletion(new OpenAiChatCompletionRequest(messagesList));

messagesList.add(result.getMessage());
messagesList.add(OpenAiMessage.user("What is the typical food there?"));

return OpenAiClient.forModel(GPT_4O_MINI)
.chatCompletion(new OpenAiChatCompletionRequest(messagesList));
}

/**
Expand All @@ -55,9 +69,7 @@ public OpenAiChatCompletionOutput chatCompletion(@Nonnull final String prompt) {
@Nonnull
public Stream<OpenAiChatCompletionDelta> streamChatCompletionDeltas(
@Nonnull final String message) {
final var request =
new OpenAiChatCompletionParameters()
.addMessages(new OpenAiChatMessage.OpenAiChatUserMessage().addText(message));
final var request = new OpenAiChatCompletionRequest(OpenAiMessage.user(message));

return OpenAiClient.forModel(GPT_4O_MINI).streamChatCompletionDeltas(request);
}
Expand All @@ -82,93 +94,50 @@ public Stream<String> streamChatCompletion(@Nonnull final String message) {
* @return the assistant message response
*/
@Nonnull
public OpenAiChatCompletionOutput chatCompletionImage(@Nonnull final String linkToImage) {
final var request =
new OpenAiChatCompletionParameters()
.addMessages(
new OpenAiChatMessage.OpenAiChatUserMessage()
.addText("Describe the following image.")
.addImage(
linkToImage,
OpenAiChatMessage.OpenAiChatUserMessage.ImageDetailLevel.HIGH));

return OpenAiClient.forModel(GPT_4O).chatCompletion(request);
public OpenAiChatCompletionResponse chatCompletionImage(@Nonnull final String linkToImage) {

final var userMessage =
OpenAiMessage.user("Describe the following image.")
.withImage(linkToImage, OpenAiImageItem.DetailLevel.HIGH);

return OpenAiClient.forModel(GPT_4O)
.chatCompletion(new OpenAiChatCompletionRequest(userMessage));
}

/**
* Executes a chat completion request to OpenAI with a tool that calculates the weather.
* Chat request to OpenAI with tool that gets the weather for a given location and unit. The tool
* executed and the result is sent back to the assistant.
*
* @param location The location to get the weather for.
* @param unit The unit of temperature to use.
* @return The assistant message response.
*/
@Nonnull
public OpenAiChatCompletionOutput chatCompletionToolExecution(
public OpenAiChatCompletionResponse chatCompletionToolExecution(
@Nonnull final String location, @Nonnull final String unit) {
final OpenAiClient client = OpenAiClient.forModel(GPT_4O_MINI);

// 1. Define the function
final Map<String, Object> schemaMap = generateSchema(WeatherMethod.Request.class);
final var function =
new OpenAiChatCompletionFunction()
.setName("weather")
.setDescription("Get the weather for the given location")
.setParameters(schemaMap);
final var tool = new OpenAiChatCompletionTool().setType(FUNCTION).setFunction(function);

final var messages = new ArrayList<OpenAiChatMessage>();
messages.add(
new OpenAiChatMessage.OpenAiChatUserMessage()
.addText("What's the weather in %s in %s?".formatted(location, unit)));

// Assistant will call the function
final var request =
new OpenAiChatCompletionParameters()
.addMessages(messages.toArray(OpenAiChatMessage[]::new))
.setTools(List.of(tool));

final OpenAiChatCompletionOutput response =
OpenAiClient.forModel(GPT_4O_MINI).chatCompletion(request);

// 2. Optionally, execute the function.
final OpenAiChatToolCall toolCall =
response.getChoices().get(0).getMessage().getToolCalls().get(0);
final WeatherMethod.Request arguments =
parseJson(toolCall.getFunction().getArguments(), WeatherMethod.Request.class);
final WeatherMethod.Response currentWeather = WeatherMethod.getCurrentWeather(arguments);

final OpenAiChatMessage.OpenAiChatAssistantMessage assistantMessage =
response.getChoices().get(0).getMessage();
messages.add(assistantMessage);

final var toolMessage =
new OpenAiChatMessage.OpenAiChatToolMessage()
.setToolCallId(toolCall.getId())
.setContent(currentWeather.toString());
messages.add(toolMessage);

final var finalRequest =
new OpenAiChatCompletionParameters()
.addMessages(messages.toArray(OpenAiChatMessage[]::new));

return OpenAiClient.forModel(GPT_4O_MINI).chatCompletion(finalRequest);
}
final var messages = new ArrayList<OpenAiMessage>();
messages.add(OpenAiMessage.user("What's the weather in %s in %s?".formatted(location, unit)));

private static <T> T parseJson(@Nonnull final String rawJson, @Nonnull final Class<T> clazz) {
try {
return JACKSON.readValue(rawJson, clazz);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Failed to parse tool call arguments: " + rawJson, e);
}
}

private static Map<String, Object> generateSchema(@Nonnull final Class<?> clazz) {
final var jsonSchemaGenerator = new JsonSchemaGenerator(JACKSON);
try {
final var schema = jsonSchemaGenerator.generateSchema(clazz);
return JACKSON.convertValue(schema, new TypeReference<>() {});
} catch (JsonMappingException e) {
throw new IllegalArgumentException("Could not generate schema for " + clazz.getName(), e);
}
// 1. Define the function
final List<OpenAiTool> tools =
List.of(
OpenAiTool.forFunction(WeatherMethod::getCurrentWeather)
.withArgument(WeatherMethod.Request.class)
.withName("weather")
.withDescription("Get the weather for the given location"));

// 2. Assistant calls the function
final var request = new OpenAiChatCompletionRequest(messages).withToolsExecutable(tools);
final OpenAiChatCompletionResponse response = client.chatCompletion(request);

// 3. Execute the tool calls
messages.add(response.getMessage());
messages.addAll(response.executeTools());

// 4. Have model run the final request with incorporated tool results
return client.chatCompletion(request.withMessages(messages));
}

/**
Expand All @@ -178,8 +147,8 @@ private static Map<String, Object> generateSchema(@Nonnull final Class<?> clazz)
* @return the embedding response
*/
@Nonnull
public OpenAiEmbeddingOutput embedding(@Nonnull final String input) {
final var request = new OpenAiEmbeddingParameters().setInput(input);
public OpenAiEmbeddingResponse embedding(@Nonnull final String input) {
final var request = new OpenAiEmbeddingRequest(List.of(input));

return OpenAiClient.forModel(TEXT_EMBEDDING_3_SMALL).embedding(request);
}
Expand All @@ -192,12 +161,13 @@ public OpenAiEmbeddingOutput embedding(@Nonnull final String input) {
* @return the assistant message response
*/
@Nonnull
public OpenAiChatCompletionOutput chatCompletionWithResource(
public OpenAiChatCompletionResponse chatCompletionWithResource(
@Nonnull final String resourceGroup, @Nonnull final String prompt) {

final var destination =
new AiCoreService().getInferenceDestination(resourceGroup).forModel(GPT_4O);

return OpenAiClient.withCustomDestination(destination).chatCompletion(prompt);
return OpenAiClient.withCustomDestination(destination)
.chatCompletion(new OpenAiChatCompletionRequest(prompt));
}
}
Loading