Skip to content

Commit d15ee63

Browse files
committed
Change withOpenAiTools to withToolsExecutable; Change OpenAiToolExecutor.executeTools to OpenAiTool.execute; Change mandatory argument value assistantMessage.toolCalls() to assistantMessage; Add intermediate API to optionally extract original execution result objects to getMessages() convenience method
1 parent c4bfa20 commit d15ee63

File tree

7 files changed

+145
-125
lines changed

7 files changed

+145
-125
lines changed

foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiChatCompletionRequest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,8 @@ public OpenAiChatCompletionRequest withToolChoice(@Nonnull final OpenAiToolChoic
291291
* @since 1.7.0
292292
*/
293293
@Nonnull
294-
public OpenAiChatCompletionRequest withOpenAiTools(@Nonnull final List<OpenAiTool<?>> tools) {
294+
@Beta
295+
public OpenAiChatCompletionRequest withToolsExecutable(@Nonnull final List<OpenAiTool<?>> tools) {
295296
return this.withTools(tools.stream().map(OpenAiTool::createChatCompletionTool).toList());
296297
}
297298

foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiFunctionCall.java

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@
33
import static com.sap.ai.sdk.foundationmodels.openai.OpenAiUtils.getOpenAiObjectMapper;
44

55
import com.fasterxml.jackson.core.JsonProcessingException;
6-
import com.fasterxml.jackson.core.type.TypeReference;
76
import com.google.common.annotations.Beta;
8-
import java.lang.reflect.Type;
97
import java.util.Map;
108
import javax.annotation.Nonnull;
119
import lombok.AllArgsConstructor;
@@ -38,40 +36,25 @@ public class OpenAiFunctionCall implements OpenAiToolCall {
3836
*/
3937
@Nonnull
4038
public Map<String, Object> getArgumentsAsMap() throws IllegalArgumentException {
41-
return parseArguments(new TypeReference<>() {});
39+
return getArgumentsAsObject(Map.class);
4240
}
4341

4442
/**
4543
* Parses the arguments, encoded as a JSON string, into an object of type expected by a function
4644
* tool.
4745
*
48-
* @param tool the function tool the arguments are for
46+
* @param type the class reference for requested type.
4947
* @param <T> the type of the class
5048
* @return the parsed arguments as an object
5149
* @throws IllegalArgumentException if parsing fails
5250
* @since 1.7.0
5351
*/
5452
@Nonnull
55-
public <T> T getArgumentsAsObject(@Nonnull final OpenAiTool<T> tool)
56-
throws IllegalArgumentException {
57-
58-
return parseArguments(
59-
new TypeReference<>() {
60-
@Override
61-
public Type getType() {
62-
return tool.getRequestClass();
63-
}
64-
});
65-
}
66-
67-
@Nonnull
68-
private <T> T parseArguments(@Nonnull final TypeReference<T> typeReference)
69-
throws IllegalArgumentException {
53+
public <T> T getArgumentsAsObject(@Nonnull final Class<T> type) throws IllegalArgumentException {
7054
try {
71-
return getOpenAiObjectMapper().readValue(getArguments(), typeReference);
55+
return getOpenAiObjectMapper().readValue(getArguments(), type);
7256
} catch (JsonProcessingException e) {
73-
throw new IllegalArgumentException(
74-
"Failed to parse JSON string to class " + typeReference.getType(), e);
57+
throw new IllegalArgumentException("Failed to parse JSON string to class " + type, e);
7558
}
7659
}
7760
}

foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiTool.java

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package com.sap.ai.sdk.foundationmodels.openai;
22

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

6+
import com.fasterxml.jackson.core.JsonProcessingException;
57
import com.fasterxml.jackson.core.type.TypeReference;
8+
import com.fasterxml.jackson.databind.ObjectMapper;
69
import com.github.victools.jsonschema.generator.Option;
710
import com.github.victools.jsonschema.generator.OptionPreset;
811
import com.github.victools.jsonschema.generator.SchemaGenerator;
@@ -13,15 +16,21 @@
1316
import com.google.common.annotations.Beta;
1417
import com.sap.ai.sdk.foundationmodels.openai.generated.model.ChatCompletionTool;
1518
import com.sap.ai.sdk.foundationmodels.openai.generated.model.FunctionObject;
19+
import java.util.ArrayList;
20+
import java.util.LinkedHashMap;
21+
import java.util.List;
1622
import java.util.Map;
1723
import java.util.function.Function;
24+
import java.util.stream.Collectors;
1825
import javax.annotation.Nonnull;
1926
import javax.annotation.Nullable;
2027
import lombok.AccessLevel;
2128
import lombok.AllArgsConstructor;
2229
import lombok.Data;
2330
import lombok.Getter;
31+
import lombok.RequiredArgsConstructor;
2432
import lombok.experimental.Accessors;
33+
import lombok.extern.slf4j.Slf4j;
2534

2635
/**
2736
* Represents an OpenAI tool that can be used to define a function call in an OpenAI Chat Completion
@@ -32,13 +41,16 @@
3241
* @see <a href="https://platform.openai.com/docs/guides/gpt/function-calling"/>OpenAI Function
3342
* @since 1.7.0
3443
*/
44+
@Slf4j
3545
@Beta
3646
@Data
3747
@Getter(AccessLevel.PACKAGE)
3848
@Accessors(chain = true)
3949
@AllArgsConstructor(access = AccessLevel.PRIVATE)
4050
public class OpenAiTool<InputT> {
4151

52+
private static final ObjectMapper JACKSON = new ObjectMapper();
53+
4254
/** The schema generator used to create JSON schemas. */
4355
@Nonnull private static final SchemaGenerator GENERATOR = createSchemaGenerator();
4456

@@ -71,7 +83,8 @@ public OpenAiTool(@Nonnull final String name, @Nonnull final Class<InputT> reque
7183
@Nonnull
7284
Object execute(@Nonnull final InputT argument) {
7385
if (getFunction() == null) {
74-
throw new IllegalStateException("No function configured to execute.");
86+
throw new IllegalStateException(
87+
"Tool " + name + " is missing a method reference to execute.");
7588
}
7689
return getFunction().apply(argument);
7790
}
@@ -102,4 +115,78 @@ private static SchemaGenerator createSchemaGenerator() {
102115
.with(module)
103116
.build());
104117
}
118+
119+
/**
120+
* Executes the given tool calls with the provided tools and returns the results as a list of
121+
* {@link OpenAiToolMessage} containing execution results encoded as JSON string.
122+
*
123+
* @param tools the list of tools to execute
124+
* @param msg the assistant message containing a list of tool calls with arguments
125+
* @return a result object that contains the list of tool messages with the results
126+
* @throws IllegalStateException if a tool is missing a method reference for function execution.
127+
*/
128+
@Beta
129+
@Nonnull
130+
public static Execution execute(
131+
@Nonnull final List<OpenAiTool<?>> tools, @Nonnull final OpenAiAssistantMessage msg)
132+
throws IllegalArgumentException {
133+
final var result = new LinkedHashMap<OpenAiFunctionCall, Object>();
134+
135+
final var toolMap = tools.stream().collect(Collectors.toMap(OpenAiTool::getName, identity()));
136+
for (final OpenAiToolCall toolCall : msg.toolCalls()) {
137+
if (toolCall instanceof OpenAiFunctionCall functionCall) {
138+
final var tool = toolMap.get(functionCall.getName());
139+
if (tool == null) {
140+
log.warn("Tool not found for function call: {}", functionCall.getName());
141+
continue;
142+
}
143+
final var toolResult = executeFunction(tool, functionCall);
144+
result.put(functionCall, toolResult);
145+
}
146+
}
147+
return new Execution(result);
148+
}
149+
150+
@Nonnull
151+
private static <I> Object executeFunction(
152+
@Nonnull final OpenAiTool<I> tool, @Nonnull final OpenAiFunctionCall toolCall) {
153+
final I arguments = toolCall.getArgumentsAsObject(tool.getRequestClass());
154+
return tool.execute(arguments);
155+
}
156+
157+
@Nonnull
158+
private static String serializeObject(@Nonnull final Object obj) throws IllegalArgumentException {
159+
try {
160+
return JACKSON.writeValueAsString(obj);
161+
} catch (JsonProcessingException e) {
162+
throw new IllegalArgumentException("Failed to serialize object to JSON", e);
163+
}
164+
}
165+
166+
/**
167+
* Represents the result of executing a tool call, containing the results of the function calls.
168+
*/
169+
@RequiredArgsConstructor
170+
@Beta
171+
public static class Execution {
172+
@Getter @Beta @Nonnull private final Map<OpenAiFunctionCall, Object> results;
173+
174+
/**
175+
* Creates a new list of serialized OpenAI tool messages.
176+
*
177+
* @return the list of serialized OpenAI tool messages.
178+
* @throws IllegalArgumentException if the tool results cannot be serialized to JSON
179+
*/
180+
@Beta
181+
@Nonnull
182+
public List<OpenAiToolMessage> getMessages() {
183+
final var result = new ArrayList<OpenAiToolMessage>();
184+
for (final var entry : getResults().entrySet()) {
185+
final var functionCall = entry.getKey().getId();
186+
final var serializedValue = serializeObject(entry.getValue());
187+
result.add(OpenAiMessage.tool(serializedValue, functionCall));
188+
}
189+
return result;
190+
}
191+
}
105192
}

foundation-models/openai/src/main/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiToolExecutor.java

Lines changed: 0 additions & 68 deletions
This file was deleted.

foundation-models/openai/src/test/java/com/sap/ai/sdk/foundationmodels/openai/OpenAiChatCompletionRequestTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ record DummyRequest(String param1, int param2) {}
123123

124124
var request =
125125
new OpenAiChatCompletionRequest(OpenAiMessage.user("Hello, world"))
126-
.withOpenAiTools(
126+
.withToolsExecutable(
127127
List.of(
128128
new OpenAiTool<>("toolA", DummyRequest.class)
129129
.setDescription("descA")

0 commit comments

Comments
 (0)