diff --git a/docs/release_notes.md b/docs/release_notes.md
index aff176462..8167109df 100644
--- a/docs/release_notes.md
+++ b/docs/release_notes.md
@@ -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)
diff --git a/foundation-models/openai/pom.xml b/foundation-models/openai/pom.xml
index ce3cd93b1..0e880a326 100644
--- a/foundation-models/openai/pom.xml
+++ b/foundation-models/openai/pom.xml
@@ -77,6 +77,14 @@
com.fasterxml.jackson.core
jackson-annotations
+
+ com.github.victools
+ jsonschema-generator
+
+
+ com.github.victools
+ jsonschema-module-jackson
+
io.vavr
vavr
diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiChatCompletionRequest.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiChatCompletionRequest.java
index 5f308a1c4..427749495 100644
--- a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiChatCompletionRequest.java
+++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiChatCompletionRequest.java
@@ -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;
@@ -125,6 +126,15 @@ public class OpenAiChatCompletionRequest {
/** List of tools that the model may invoke during the completion. */
@Nullable List tools;
+ /**
+ * List of tools that are executable at runtime of the application.
+ *
+ * @since 1.7.0
+ */
+ @Getter(value = AccessLevel.PACKAGE)
+ @Nullable
+ List toolsExecutable;
+
/** Option to control which tool is invoked by the model. */
@With(AccessLevel.PRIVATE)
@Nullable
@@ -179,6 +189,7 @@ public OpenAiChatCompletionRequest(@Nonnull final List messages)
null,
null,
null,
+ null,
null);
}
@@ -226,6 +237,7 @@ public OpenAiChatCompletionRequest withParallelToolCalls(
this.streamOptions,
this.responseFormat,
this.tools,
+ this.toolsExecutable,
this.toolChoice);
}
@@ -258,6 +270,7 @@ public OpenAiChatCompletionRequest withLogprobs(@Nonnull final Boolean logprobs)
this.streamOptions,
this.responseFormat,
this.tools,
+ this.toolsExecutable,
this.toolChoice);
}
@@ -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 getChatCompletionTools() {
+ final var toolsCombined = new ArrayList();
+ 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;
+ }
}
diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiChatCompletionResponse.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiChatCompletionResponse.java
index c1d0bb772..35fdc3e5e 100644
--- a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiChatCompletionResponse.java
+++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiChatCompletionResponse.java
@@ -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;
@@ -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.
@@ -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 executeTools() {
+ final var tools = originalRequest.getToolsExecutable();
+ return OpenAiTool.execute(tools != null ? tools : List.of(), getMessage());
+ }
}
diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java
index 7cfcadb35..88f8513a0 100644
--- a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java
+++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiClient.java
@@ -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);
}
/**
diff --git a/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiTool.java b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiTool.java
new file mode 100644
index 000000000..3d2e7e900
--- /dev/null
+++ b/foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiTool.java
@@ -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 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 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 the type of the function input-argument class.
+ */
+ @Nonnull
+ public static Builder1 forFunction(@Nonnull final Function function) {
+ return inputClass ->
+ name -> {
+ final Function 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 the type of the input class.
+ */
+ public interface Builder1 {
+ /**
+ * 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 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 deserializeArgument(@Nonnull final Class 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