Skip to content
Merged
Show file tree
Hide file tree
Changes from 31 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
2 changes: 1 addition & 1 deletion docs/release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +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)

### 📈 Improvements

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 @@ -282,6 +282,20 @@ public OpenAiChatCompletionRequest withToolChoice(@Nonnull final OpenAiToolChoic
return this.withToolChoice(choice.toolChoice);
}

/**
* Sets the tools to be used in the request with convenience class {@code OpenAiTool}.
*
* @param tools the list of tools to be used
* @return a new OpenAiChatCompletionRequest instance with the specified tools
* @throws IllegalArgumentException if the tool type is not supported
* @since 1.7.0
*/
@Nonnull
@Beta
public OpenAiChatCompletionRequest withToolsExecutable(@Nonnull final List<OpenAiTool<?>> tools) {
return this.withTools(tools.stream().map(OpenAiTool::createChatCompletionTool).toList());
}

/**
* Converts the request to a generated model class CreateChatCompletionRequest.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
@Setter(value = NONE)
public class OpenAiChatCompletionResponse {
/** The original response from the OpenAI API. */
@Nonnull final CreateChatCompletionResponse originalResponse;
@Nonnull CreateChatCompletionResponse originalResponse;

/**
* Gets the token usage from the original response.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package com.sap.ai.sdk.foundationmodels.openai;

import static com.sap.ai.sdk.foundationmodels.openai.OpenAiUtils.getOpenAiObjectMapper;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.annotations.Beta;
import java.util.Map;
import javax.annotation.Nonnull;
import lombok.AllArgsConstructor;
import lombok.Value;
Expand All @@ -22,4 +26,35 @@ public class OpenAiFunctionCall implements OpenAiToolCall {

/** The arguments for the function call, encoded as a JSON string. */
@Nonnull String arguments;

/**
* Parses the arguments, encoded as a JSON string, into a {@code Map<String, Object>}.
*
* @return a map of the arguments
* @throws IllegalArgumentException if parsing fails
* @since 1.7.0
*/
@Nonnull
public Map<String, Object> getArgumentsAsMap() throws IllegalArgumentException {
return getArgumentsAsObject(Map.class);
}

/**
* Parses the arguments, encoded as a JSON string, into an object of type expected by a function
* tool.
*
* @param type the class reference for requested type.
* @param <T> the type of the class
* @return the parsed arguments as an object
* @throws IllegalArgumentException if parsing fails
* @since 1.7.0
*/
@Nonnull
public <T> T getArgumentsAsObject(@Nonnull final Class<T> type) throws IllegalArgumentException {
try {
return getOpenAiObjectMapper().readValue(getArguments(), type);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Failed to parse JSON string to class " + type, e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
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.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.Data;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;
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.
*
* @param <InputT> the type of the input argument for the function
* @see <a href="https://platform.openai.com/docs/guides/gpt/function-calling"/>OpenAI Function
* @since 1.7.0
*/
@Slf4j
@Beta
@Data
@Getter(AccessLevel.PACKAGE)
@Accessors(chain = true)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class OpenAiTool<InputT> {

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. */
@Nonnull private String name;

/** The model class for function request. */
@Nonnull private Class<InputT> requestClass;

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

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

/** The function to be called. */
@Nullable private Function<InputT, ?> function;

/**
* Constructs an {@code OpenAiFunctionTool} with the specified name and a model class that
* captures the request to the function.
*
* @param name the name of the function
* @param requestClass the model class for function request
*/
public OpenAiTool(@Nonnull final String name, @Nonnull final Class<InputT> requestClass) {
this(name, requestClass, null, null, null);
}

@Nonnull
Object execute(@Nonnull final InputT argument) {
if (getFunction() == null) {
throw new IllegalStateException(
"Tool " + name + " is missing a method reference to execute.");
}
return getFunction().apply(argument);
}

ChatCompletionTool createChatCompletionTool() {
final var schema = GENERATOR.generateSchema(getRequestClass());
final var schemaMap =
OpenAiUtils.getOpenAiObjectMapper()
.convertValue(schema, 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 a result object that contains the list of tool messages with the results
* @throws IllegalStateException if a tool is missing a method reference for function execution.
*/
@Beta
@Nonnull
public static Execution execute(
@Nonnull final List<OpenAiTool<?>> tools, @Nonnull final OpenAiAssistantMessage msg)
throws IllegalArgumentException {
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 new Execution(result);
}

@Nonnull
private static <I> Object executeFunction(
@Nonnull final OpenAiTool<I> tool, @Nonnull final OpenAiFunctionCall toolCall) {
final I arguments = toolCall.getArgumentsAsObject(tool.getRequestClass());
return tool.execute(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);
}
}

/**
* Represents the result of executing a tool call, containing the results of the function calls.
*/
@RequiredArgsConstructor
@Beta
public static class Execution {
@Getter @Beta @Nonnull private final Map<OpenAiFunctionCall, Object> results;

/**
* Creates a new list of serialized OpenAI tool messages.
*
* @return the list of serialized OpenAI tool messages.
* @throws IllegalArgumentException if the tool results cannot be serialized to JSON
*/
@Beta
@Nonnull
public List<OpenAiToolMessage> getMessages() {
final var result = new ArrayList<OpenAiToolMessage>();
for (final var entry : getResults().entrySet()) {
final var functionCall = entry.getKey().getId();
final var serializedValue = serializeObject(entry.getValue());
result.add(OpenAiMessage.tool(serializedValue, functionCall));
}
return result;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@

import com.sap.ai.sdk.foundationmodels.openai.generated.model.ChatCompletionRequestUserMessage;
import com.sap.ai.sdk.foundationmodels.openai.generated.model.ChatCompletionRequestUserMessageContent;
import com.sap.ai.sdk.foundationmodels.openai.generated.model.ChatCompletionTool;
import com.sap.ai.sdk.foundationmodels.openai.generated.model.ChatCompletionToolChoiceOption;
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 org.junit.jupiter.api.Test;

class OpenAiChatCompletionRequestTest {
Expand Down Expand Up @@ -114,4 +116,40 @@ void messageListExternallyUnmodifiable() {
.as("Modifying the original list should not affect the messages in the request object.")
.hasSize(1);
}

@Test
void withOpenAiTools() {
record DummyRequest(String param1, int param2) {}

var request =
new OpenAiChatCompletionRequest(OpenAiMessage.user("Hello, world"))
.withToolsExecutable(
List.of(
new OpenAiTool<>("toolA", DummyRequest.class)
.setDescription("descA")
.setStrict(true),
new OpenAiTool<>("toolB", DummyRequest.class)
.setDescription("descB")
.setStrict(false)));

var lowLevelRequest = request.createCreateChatCompletionRequest();
assertThat(lowLevelRequest.getTools()).hasSize(2);

var toolA = lowLevelRequest.getTools().get(0);
assertThat(toolA).isInstanceOf(ChatCompletionTool.class);
assertThat(toolA.getType()).isEqualTo(ChatCompletionTool.TypeEnum.FUNCTION);
assertThat(toolA.getFunction().getName()).isEqualTo("toolA");
assertThat(toolA.getFunction().getDescription()).isEqualTo("descA");
assertThat(toolA.getFunction().isStrict()).isTrue();

assertThat(toolA.getFunction().getParameters())
.isEqualTo(
Map.of(
"properties",
Map.of(
"param1", Map.of("type", "string"),
"param2", Map.of("type", "integer")),
"type",
"object"));
}
}
Loading