Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
6ebb2db
feat: add methods to convert function call arguments to Map and speci…
rpanackal Apr 7, 2025
f7e9088
feat: introduce OpenAiFunctionTool and enhance OpenAiChatCompletionRe…
rpanackal Apr 7, 2025
75fe7dd
docs: enhance OpenAiFunctionTool and related classes documentation
rpanackal Apr 7, 2025
e726226
Merge remote-tracking branch 'refs/remotes/origin/main' into feat/ope…
rpanackal Apr 7, 2025
484c14d
docs: add @since 1.7.0 annotations to relevant methods for version tr…
rpanackal Apr 7, 2025
58792c6
ci
rpanackal Apr 7, 2025
cd03dac
fix unintentional javadoc change
rpanackal Apr 7, 2025
c92954a
test: update Javadoc to include exception details and enhance test co…
rpanackal Apr 8, 2025
03453a4
refactor: enhance JSON parsing methods in OpenAiFunctionCall and remo…
rpanackal Apr 8, 2025
36ed5bd
refactor: simplify JSON parsing in OpenAiFunctionCall and add unit te…
rpanackal Apr 8, 2025
33f2ca8
refactor: update OpenAiFunctionTool to improve argument parsing and e…
rpanackal Apr 8, 2025
87faa45
Merge branch 'main' into feat/openai/tool-definition-and-call-parsing
CharlesDuboisSAP Apr 9, 2025
7ce6aef
part done
rpanackal Apr 9, 2025
bfce048
Merge remote-tracking branch 'origin/feat/openai/tool-definition-and-…
CharlesDuboisSAP Apr 9, 2025
57ba2a1
finito
CharlesDuboisSAP Apr 9, 2025
36b0189
Formatting
bot-sdk-js Apr 9, 2025
bcb42c8
With purely generics
rpanackal Apr 10, 2025
c52fafa
With purely explicit types
rpanackal Apr 10, 2025
66cef01
refactor: enhance response serialization and improve schema generatio…
rpanackal Apr 10, 2025
3038eb3
Introduce OpenAiToolExecutor
rpanackal Apr 11, 2025
c53b04e
CI
rpanackal Apr 11, 2025
27fa975
test: OpenAiToolExecutor and improve error messages
rpanackal Apr 15, 2025
e8764c8
docs: update release notes to include tool execution in OpenAI functi…
rpanackal Apr 15, 2025
9d3684d
Merge remote-tracking branch 'refs/remotes/origin/main' into feat/ope…
rpanackal Apr 15, 2025
ad89753
fix: use the same schema generation dependency as in orchestration mo…
rpanackal Apr 17, 2025
f8e3645
refactor: rename generic type parameter from I to InputT in OpenAiTool
rpanackal Apr 17, 2025
2da40a0
Merge branch 'refs/heads/main' into feat/openai/tool-definition-and-c…
rpanackal Apr 17, 2025
c4bfa20
fix shadowing problem
rpanackal Apr 17, 2025
d15ee63
Change `withOpenAiTools` to `withToolsExecutable`; Change `OpenAiTool…
a-d Apr 28, 2025
91433a2
Merge remote-tracking branch 'origin/main' into feat/openai/tool-defi…
a-d Apr 28, 2025
2239ad7
Merge branch 'main' into feat/openai/tool-definition-and-call-parsing-v2
newtork Apr 28, 2025
f297fed
Update sample-code/spring-app/src/main/java/com/sap/ai/sdk/app/servic…
newtork Apr 30, 2025
7baf530
Merge branch 'feat/openai/tool-definition-and-call-parsing-v2' of htt…
a-d Apr 30, 2025
9f96e1a
Remove generic type argument from OpenAiTool class; Migrate error-pro…
a-d Apr 30, 2025
ce3ba95
Remove unnecessary throws declaration
a-d Apr 30, 2025
3667b1e
Reverted the intermediate result class; `OpenAiTool#execute` will dir…
a-d Apr 30, 2025
2de2592
Merge remote-tracking branch 'origin/main' into feat/openai/tool-defi…
a-d May 14, 2025
454f7c5
Remove unused code
a-d May 14, 2025
ed01c22
Add convenience method OpenAiChatCompletionResponse#executeTools
a-d May 14, 2025
1f4d2a2
Formatting
bot-sdk-js May 14, 2025
6bd64ff
Hide getter
a-d May 14, 2025
ca7fc25
Fix PMD
a-d May 14, 2025
3e69967
Fix tests
a-d May 14, 2025
8f2346d
Make inaccessible method private
newtork May 14, 2025
c21ef75
Reduce visibility of OpenAiTool#execute
newtork May 14, 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
1 change: 1 addition & 0 deletions docs/release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

### ✨ New Functionality

- [OpenAI] [Add convenience for tool definition, parsing function calls and tool execution](https://sap.github.io/ai-sdk/docs/java/foundation-models/openai/chat-completion#executing-tool-calls)
- [Orchestration] Added new model DeepSeek-R1: `OrchestrationAiModel.DEEPSEEK_R1`
- [Orchestration] [Tool execution fully enabled](https://sap.github.io/ai-sdk/docs/java/spring-ai/orchestration#tool-calling)

Expand Down
8 changes: 8 additions & 0 deletions foundation-models/openai/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.github.victools</groupId>
<artifactId>jsonschema-generator</artifactId>
</dependency>
<dependency>
<groupId>com.github.victools</groupId>
<artifactId>jsonschema-module-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.vavr</groupId>
<artifactId>vavr</artifactId>
Expand Down
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.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
Expand Down Expand Up @@ -125,6 +126,15 @@ public class OpenAiChatCompletionRequest {
/** List of tools that the model may invoke during the completion. */
@Nullable List<ChatCompletionTool> tools;

/**
* List of tools that are executable at runtime of the application.
*
* @since 1.7.0
*/
@Getter(value = AccessLevel.PACKAGE)
@Nullable
List<OpenAiTool> toolsExecutable;

/** Option to control which tool is invoked by the model. */
@With(AccessLevel.PRIVATE)
@Nullable
Expand Down Expand Up @@ -179,6 +189,7 @@ public OpenAiChatCompletionRequest(@Nonnull final List<OpenAiMessage> messages)
null,
null,
null,
null,
null);
}

Expand Down Expand Up @@ -226,6 +237,7 @@ public OpenAiChatCompletionRequest withParallelToolCalls(
this.streamOptions,
this.responseFormat,
this.tools,
this.toolsExecutable,
this.toolChoice);
}

Expand Down Expand Up @@ -258,6 +270,7 @@ public OpenAiChatCompletionRequest withLogprobs(@Nonnull final Boolean logprobs)
this.streamOptions,
this.responseFormat,
this.tools,
this.toolsExecutable,
this.toolChoice);
}

Expand Down Expand Up @@ -312,10 +325,24 @@ CreateChatCompletionRequest createCreateChatCompletionRequest() {
request.seed(this.seed);
request.streamOptions(this.streamOptions);
request.responseFormat(this.responseFormat);
request.tools(this.tools);
request.tools(getChatCompletionTools());
request.toolChoice(this.toolChoice);
request.functionCall(null);
request.functions(null);
return request;
}

@Nullable
private List<ChatCompletionTool> getChatCompletionTools() {
final var toolsCombined = new ArrayList<ChatCompletionTool>();
if (this.tools != null) {
toolsCombined.addAll(this.tools);
}
if (this.toolsExecutable != null) {
for (final OpenAiTool tool : this.toolsExecutable) {
toolsCombined.add(tool.createChatCompletionTool());
}
}
return toolsCombined.isEmpty() ? null : toolsCombined;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.util.List;
import java.util.Objects;
import javax.annotation.Nonnull;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.Value;
Expand All @@ -26,7 +27,12 @@
@Setter(value = NONE)
public class OpenAiChatCompletionResponse {
/** The original response from the OpenAI API. */
@Nonnull final CreateChatCompletionResponse originalResponse;
@Nonnull CreateChatCompletionResponse originalResponse;

/** The original request that was sent to the OpenAI API. */
@Getter(NONE)
@Nonnull
OpenAiChatCompletionRequest originalRequest;

/**
* Gets the token usage from the original response.
Expand Down Expand Up @@ -96,4 +102,16 @@ public OpenAiAssistantMessage getMessage() {

return new OpenAiAssistantMessage(new OpenAiMessageContent(contentItems), openAiToolCalls);
}

/**
* Execute tool calls that were suggested by the assistant response.
*
* @return the list of tool messages that were serialized for the computed results. Empty list if
* no tools were called.
*/
@Nonnull
public List<OpenAiToolMessage> executeTools() {
final var tools = originalRequest.getToolsExecutable();
return OpenAiTool.execute(tools != null ? tools : List.of(), getMessage());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,8 @@ public OpenAiChatCompletionOutput chatCompletion(@Nonnull final String prompt)
public OpenAiChatCompletionResponse chatCompletion(
@Nonnull final OpenAiChatCompletionRequest request) throws OpenAiClientException {
warnIfUnsupportedUsage();
return new OpenAiChatCompletionResponse(
chatCompletion(request.createCreateChatCompletionRequest()));
final var response = chatCompletion(request.createCreateChatCompletionRequest());
return new OpenAiChatCompletionResponse(response, request);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
package com.sap.ai.sdk.foundationmodels.openai;

import static com.sap.ai.sdk.foundationmodels.openai.generated.model.ChatCompletionTool.TypeEnum.FUNCTION;
import static java.util.function.UnaryOperator.identity;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.github.victools.jsonschema.generator.Option;
import com.github.victools.jsonschema.generator.OptionPreset;
import com.github.victools.jsonschema.generator.SchemaGenerator;
import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
import com.github.victools.jsonschema.generator.SchemaVersion;
import com.github.victools.jsonschema.module.jackson.JacksonModule;
import com.github.victools.jsonschema.module.jackson.JacksonOption;
import com.google.common.annotations.Beta;
import com.sap.ai.sdk.foundationmodels.openai.generated.model.ChatCompletionTool;
import com.sap.ai.sdk.foundationmodels.openai.generated.model.FunctionObject;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.Value;
import lombok.With;
import lombok.extern.slf4j.Slf4j;

/**
* Represents an OpenAI tool that can be used to define a function call in an OpenAI Chat Completion
* request. This tool generates a JSON schema based on the provided class representing the
* function's request structure.
*
* @see <a href="https://platform.openai.com/docs/guides/gpt/function-calling"/>OpenAI Function
* @since 1.7.0
*/
@Slf4j
@Beta
@Value
@With
@Getter(AccessLevel.PACKAGE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class OpenAiTool {

private static final ObjectMapper JACKSON = new ObjectMapper();

/** The schema generator used to create JSON schemas. */
@Nonnull private static final SchemaGenerator GENERATOR = createSchemaGenerator();

/** The name of the function. */
@Setter(AccessLevel.NONE)
@Nonnull
String name;

/** The function to execute a string argument to tool result object. */
@Setter(AccessLevel.NONE)
@Nonnull
Function<String, Object> functionExecutor;

/** schema to be used for the function call. */
@Setter(AccessLevel.NONE)
@Nonnull
ObjectNode schema;

/** An optional description of the function. */
@Nullable String description;

/** An optional flag indicating whether the function parameters should be treated strictly. */
@Nullable Boolean strict;

/**
* Instantiates a OpenAiTool builder instance on behalf of an executable function.
*
* @param function the function to be executed.
* @return an OpenAiTool builder instance.
* @param <InputT> the type of the function input-argument class.
*/
@Nonnull
public static <InputT> Builder1<InputT> forFunction(@Nonnull final Function<InputT, ?> function) {
return inputClass ->
name -> {
final Function<String, Object> exec =
s -> function.apply(deserializeArgument(inputClass, s));
final var schema = GENERATOR.generateSchema(inputClass);
return new OpenAiTool(name, exec, schema, null, null);
};
}

/**
* Creates a new OpenAiTool instance with the specified function and input class.
*
* @param <InputT> the type of the input class.
*/
public interface Builder1<InputT> {
/**
* Sets the name of the function.
*
* @param inputClass the class of the input object.
* @return a new OpenAiTool instance with the specified function and input class.
*/
@Nonnull
Builder2 withArgument(@Nonnull final Class<InputT> inputClass);
}

/** Creates a new OpenAiTool instance with the specified name. */
public interface Builder2 {
/**
* Sets the name of the function.
*
* @param name the name of the function
* @return a new OpenAiTool instance with the specified name
*/
@Nonnull
OpenAiTool withName(@Nonnull final String name);
}

@Nullable
static <T> T deserializeArgument(@Nonnull final Class<T> cl, @Nonnull final String s) {
try {
return JACKSON.readValue(s, cl);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Failed to parse JSON string to class " + cl, e);
}
}

ChatCompletionTool createChatCompletionTool() {
final var schemaMap =
OpenAiUtils.getOpenAiObjectMapper()
.convertValue(getSchema(), new TypeReference<Map<String, Object>>() {});

return new ChatCompletionTool()
.type(FUNCTION)
.function(
new FunctionObject()
.name(getName())
.description(getDescription())
.parameters(schemaMap)
.strict(getStrict()));
}

private static SchemaGenerator createSchemaGenerator() {
final var module =
new JacksonModule(
JacksonOption.RESPECT_JSONPROPERTY_REQUIRED, JacksonOption.RESPECT_JSONPROPERTY_ORDER);
return new SchemaGenerator(
new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON)
.without(Option.SCHEMA_VERSION_INDICATOR)
.with(module)
.build());
}

/**
* Executes the given tool calls with the provided tools and returns the results as a list of
* {@link OpenAiToolMessage} containing execution results encoded as JSON string.
*
* @param tools the list of tools to execute
* @param msg the assistant message containing a list of tool calls with arguments
* @return The list of tool messages with the results.
*/
@Beta
@Nonnull
static List<OpenAiToolMessage> execute(
@Nonnull final List<OpenAiTool> tools, @Nonnull final OpenAiAssistantMessage msg) {
final var toolResults = executeInternal(tools, msg);
final var result = new ArrayList<OpenAiToolMessage>();
for (final var entry : toolResults.entrySet()) {
final var functionCall = entry.getKey().getId();
final var serializedValue = serializeObject(entry.getValue());
result.add(OpenAiMessage.tool(serializedValue, functionCall));
}
return result;
}

/**
* Executes the given tool calls with the provided tools and returns the results as a list of
* {@link OpenAiToolMessage} containing execution results encoded as JSON string.
*
* @param tools the list of tools to execute
* @param msg the assistant message containing a list of tool calls with arguments
* @return a map that contains the function calls and their respective tool results.
*/
@Nonnull
private static Map<OpenAiFunctionCall, Object> executeInternal(
@Nonnull final List<OpenAiTool> tools, @Nonnull final OpenAiAssistantMessage msg) {
final var result = new LinkedHashMap<OpenAiFunctionCall, Object>();
final var toolMap = tools.stream().collect(Collectors.toMap(OpenAiTool::getName, identity()));
for (final OpenAiToolCall toolCall : msg.toolCalls()) {
if (toolCall instanceof OpenAiFunctionCall functionCall) {
final var tool = toolMap.get(functionCall.getName());
if (tool == null) {
log.warn("Tool not found for function call: {}", functionCall.getName());
continue;
}
final var toolResult = executeFunction(tool, functionCall);
result.put(functionCall, toolResult);
}
}
return result;
}

@Nonnull
private static Object executeFunction(
@Nonnull final OpenAiTool tool, @Nonnull final OpenAiFunctionCall toolCall) {
final Function<String, Object> executor = tool.getFunctionExecutor();
final String arguments = toolCall.getArguments();
return executor.apply(arguments);
}

@Nonnull
private static String serializeObject(@Nonnull final Object obj) throws IllegalArgumentException {
try {
return JACKSON.writeValueAsString(obj);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Failed to serialize object to JSON", e);
}
}
}
Loading